Permalink
Browse files

Support IDs, add LICENSE

  • Loading branch information...
dunglas committed Aug 25, 2018
1 parent dc07a11 commit 8c2004acbf348694f6b8cc8af06ac7223c056285
Showing with 778 additions and 27 deletions.
  1. +37 −0 COPYRIGHT
  2. +661 −0 LICENSE
  3. +20 −3 README.md
  4. +2 −0 go.mod
  5. +14 −0 go.sum
  6. +3 −3 hub/publish.go
  7. +7 −5 hub/publish_test.go
  8. +13 −4 hub/resource.go
  9. +11 −2 hub/resource_test.go
  10. +2 −2 hub/subscribe.go
  11. +8 −8 hub/subscribe_test.go
@@ -0,0 +1,37 @@
Copyright (C) 2018-present Kévin Dunglas
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License, version 3,
as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
As a special exception, the copyright holders give permission to link the
code of portions of this program with the OpenSSL library under certain
conditions as described in each individual source file and distribute
linked combinations including the program with the OpenSSL library. You
must comply with the GNU Affero General Public License in all respects
for all of the code used other than as permitted herein. If you modify
file(s) with this exception, you may extend this exception to your
version of the file(s), but you are not obligated to do so. If you do not
wish to do so, delete this exception statement from your version. If you
delete this exception statement from all source files in the program,
then also delete it in the license file.
The copyright holders provide the following statement as a clarification
of the conditions of this License. This statement is not a further
restriction. It will be deemed a legal notice allowed under Section 7b,
and must accordingly be preserved in any redistribution of the Program.
This clarification applies to the Mercure licensed hereunder by
Kévin Dunglas. The Corresponding Source of the Program includes any
software that interacts with the Program and contains functionality to
provision or manage the Program for the purpose of enabling third-party
users to interact with the Program remotely through a computer network,
and any such software that is added to or combined with the Program
constitutes modification that produces a work based on the Program.
661 LICENSE

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -70,18 +70,34 @@ Note: an URL is also a valid URI template.
The hub sends updates concerning all subscribed resources matching the provided URI templates.
The hub MUST send these updates as [`text/event-stream` compliant events](https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model).
* the `id` key of the event MUST contain the IRI of the resource being updated
* the `data` key MUST contain the new version of the resource
* the `event` key MUST be set to `mercure`, it allows a hub to mix Mercure events with other kinds of Server-sent events
The resource SHOULD be represented in a format with hypermedia capabilities such as [JSON-LD](https://www.w3.org/TR/json-ld/), [Atom](https://tools.ietf.org/html/rfc4287), [XML](https://www.w3.org/XML/) or [HTML](https://html.spec.whatwg.org/).
[Web Linking](https://tools.ietf.org/html/rfc5988) SHOULD be used to indicate the IRI of the resource sent in the event.
When using Atom, XML or HTML as serialization format for the resource, the document SHOULD contain a `link` element with a `self` relation containing the IRI of the resource.
When using JSON-LD, the document SHOULD contain an `@id` property containing the IRI of the resource.
The hub SHOULD support the other [optional capabilities defined in the Server Sent Event specification](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).
Namely, events MAY include an `id` key.
The value of this id SHOULD be unique.
Using an [UUID](https://tools.ietf.org/html/rfc4122) is considered a best practice.
According to the Server Sent Event specification, in case of connection lost the subscriber will try to automatically reconnect. During the reconnection the subscriber will send the last received event id in a [`Last-Event-Id`](https://html.spec.whatwg.org/multipage/iana.html#last-event-id) HTTP header .
If such header exists, the hub MUST send to the subscriber all events published since the one having this identifier.
The hub MAY also specify the reconnection time using the `retry` key.
Example implementation of a client in JavaScript:
```javascript
const params = new URLSearchParams();
// The subscriber subscribes to updates for the https://example.com/foo resource
// and to any resource matching https://example.com/books/{name}
params.append('iri', 'https://example.com/foo');
params.append('iri', 'https://example.com/books/{name}');
params.append('iri[]', 'https://example.com/foo');
params.append('iri[]', 'https://example.com/books/{name}');
const eventSource = new EventSource(`https://hub.example.com?${params}`);
@@ -112,6 +128,7 @@ The request must be encoded using the `application/x-www-form-urlencoded` and co
* `iri`: the IRI of the updated resource
* `data`: the resources' content
* `id` (optional): the event identifier that will be used as the SSE id, if omited it MUST be generated by the hub
The request MUST also contain an `Authorization` HTTP header containing the string `Bearer ` followed by a valid [JWT token
(RFC7519)](https://tools.ietf.org/html/rfc7519) that the hub will check to ensure that the publisher is authorized to publish
2 go.mod

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
14 go.sum

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -32,13 +32,13 @@ func (h *Hub) PublishHandler(w http.ResponseWriter, r *http.Request) {
return
}
targets := make(map[string]bool, len(r.Form["target[]"]))
targets := make(map[string]struct{}, len(r.Form["target[]"]))
for _, t := range r.Form["target[]"] {
targets[t] = true
targets[t] = struct{}{}
}
// Broadcast the resource
h.resources <- NewResource(iri, data, targets)
h.resources <- NewResource(r.Form.Get("revid"), iri, data, targets)
}
// Checks the validity of the JWT
@@ -92,17 +92,19 @@ func TestPublishOk(t *testing.T) {
defer w.Done()
for {
select {
case content := <-hub.resources:
assert.Equal(t, "http://example.com/books/1", content.IRI)
assert.Equal(t, "data: Hello!\n\n", content.Data)
assert.True(t, content.Targets["foo"])
assert.True(t, content.Targets["bar"])
case r := <-hub.resources:
assert.Equal(t, "revid", r.RevID)
assert.Equal(t, "http://example.com/books/1", r.IRI)
assert.Equal(t, "data: Hello!\n\n", r.Data)
assert.Equal(t, struct{}{}, r.Targets["foo"])
assert.Equal(t, struct{}{}, r.Targets["bar"])
return
}
}
}((&wg))
form := url.Values{}
form.Add("revid", "revid")
form.Add("iri", "http://example.com/books/1")
form.Add("data", "Hello!")
form.Add("target[]", "foo")
@@ -3,22 +3,31 @@ package hub
import (
"fmt"
"strings"
uuid "github.com/satori/go.uuid"
)
// Resource contains a server-sent event
type Resource struct {
// The Internationalized Resource Identifier (RFC3987) of the resource (will most likely be an URI), prefixed by "id: "
// The unique id corresponding to this version of this resource, will be used as the SSE id
RevID string
// The Internationalized Resource Identifier (RFC3987) of the resource (will most likely be an URI)
IRI string
// Data, encoded in the sever-sent event format: every line starts with the string "data: "
// https://www.w3.org/TR/eventsource/#dispatchMessage
Data string
// Target audience
Targets map[string]bool
Targets map[string]struct{}
}
// NewResource creates a new resource and encodes the data property
func NewResource(iri string, data string, targets map[string]bool) Resource {
return Resource{iri, fmt.Sprintf("data: %s\n\n", strings.Replace(data, "\n", "\ndata: ", -1)), targets}
func NewResource(revID, iri, data string, targets map[string]struct{}) Resource {
if revID == "" {
revID = uuid.Must(uuid.NewV4()).String()
}
return Resource{revID, iri, fmt.Sprintf("data: %s\n\n", strings.Replace(data, "\n", "\ndata: ", -1)), targets}
}
@@ -3,12 +3,21 @@ package hub
import (
"testing"
"github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
)
func TestNewResource(t *testing.T) {
r := NewResource("http://example.com", "foo\nbar", map[string]bool{"baz": true, "bat": true})
r := NewResource("foo", "http://example.com", "foo\nbar", map[string]struct{}{"baz": struct{}{}, "bat": struct{}{}})
assert.Equal(t, "foo", r.RevID)
assert.Equal(t, "http://example.com", r.IRI)
assert.Equal(t, "data: foo\ndata: bar\n\n", r.Data)
assert.Equal(t, map[string]bool{"baz": true, "bat": true}, r.Targets)
assert.Equal(t, map[string]struct{}{"baz": struct{}{}, "bat": struct{}{}}, r.Targets)
}
func TestGenerateID(t *testing.T) {
r := NewResource("", "http://example.com", "foo\nbar", map[string]struct{}{"baz": struct{}{}, "bat": struct{}{}})
_, err := uuid.FromString(r.RevID)
assert.Nil(t, err)
}
@@ -79,7 +79,7 @@ func (h *Hub) SubscribeHandler(w http.ResponseWriter, r *http.Request) {
}
fmt.Fprint(w, "event: mercure\n")
fmt.Fprintf(w, "id: %s\n", resource.IRI)
fmt.Fprintf(w, "id: %s\n", resource.RevID)
fmt.Fprint(w, resource.Data)
f.Flush()
@@ -124,7 +124,7 @@ func sendHeaders(w http.ResponseWriter) {
}
// isAuthorized checks if the subscriber can access to at least one of the resource's intended targets
func isAuthorized(subscriberTargets []string, resourceTargets map[string]bool) bool {
func isAuthorized(subscriberTargets []string, resourceTargets map[string]struct{}) bool {
if len(resourceTargets) == 0 {
return true
}
@@ -60,9 +60,9 @@ func TestSubscribe(t *testing.T) {
go func() {
for {
if len(hub.subscribers) > 0 {
hub.resources <- NewResource("http://example.com/not-subscribed", "Hello World", map[string]bool{})
hub.resources <- NewResource("http://example.com/books/1", "Hello World", map[string]bool{})
hub.resources <- NewResource("http://example.com/reviews/22", "Great", map[string]bool{})
hub.resources <- NewResource("a", "http://example.com/not-subscribed", "Hello World", map[string]struct{}{})
hub.resources <- NewResource("b", "http://example.com/books/1", "Hello World", map[string]struct{}{})
hub.resources <- NewResource("c", "http://example.com/reviews/22", "Great", map[string]struct{}{})
hub.Stop()
return
@@ -76,7 +76,7 @@ func TestSubscribe(t *testing.T) {
resp := w.Result()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "event: mercure\nid: http://example.com/books/1\ndata: Hello World\n\nevent: mercure\nid: http://example.com/reviews/22\ndata: Great\n\n", w.Body.String())
assert.Equal(t, "event: mercure\nid: b\ndata: Hello World\n\nevent: mercure\nid: c\ndata: Great\n\n", w.Body.String())
}
func TestSubscribeTarget(t *testing.T) {
@@ -86,9 +86,9 @@ func TestSubscribeTarget(t *testing.T) {
go func() {
for {
if len(hub.subscribers) > 0 {
hub.resources <- NewResource("http://example.com/reviews/21", "Foo", map[string]bool{"baz": true})
hub.resources <- NewResource("http://example.com/reviews/22", "Hello World", map[string]bool{})
hub.resources <- NewResource("http://example.com/reviews/23", "Great", map[string]bool{"hello": true, "bar": true})
hub.resources <- NewResource("a", "http://example.com/reviews/21", "Foo", map[string]struct{}{"baz": struct{}{}})
hub.resources <- NewResource("b", "http://example.com/reviews/22", "Hello World", map[string]struct{}{})
hub.resources <- NewResource("c", "http://example.com/reviews/23", "Great", map[string]struct{}{"hello": struct{}{}, "bar": struct{}{}})
hub.Stop()
return
@@ -107,7 +107,7 @@ func TestSubscribeTarget(t *testing.T) {
resp := w.Result()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "event: mercure\nid: http://example.com/reviews/22\ndata: Hello World\n\nevent: mercure\nid: http://example.com/reviews/23\ndata: Great\n\n", w.Body.String())
assert.Equal(t, "event: mercure\nid: b\ndata: Hello World\n\nevent: mercure\nid: c\ndata: Great\n\n", w.Body.String())
}
// From https://github.com/go-martini/martini/blob/master/response_writer_test.go

0 comments on commit 8c2004a

Please sign in to comment.