Skip to content

Commit

Permalink
Merge branch 'release/v1.6.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
jirenius committed Feb 3, 2021
2 parents 8fb65f9 + e633760 commit 0c95d4a
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 40 deletions.
14 changes: 7 additions & 7 deletions examples/book-collection/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Book Collection Example

This is an example, written in javascript (node.js), of a RES service with collections and resource references to books, which can be created, edited and deleted.
* It exposes a collection, `bookService.books`, containing book model references.
* It exposes book models, `bookService.book.<BOOK_ID>`, of each book.
* It exposes a collection, `library.books`, containing book model references.
* It exposes book models, `library.book.<BOOK_ID>`, of each book.
* It allows setting the books' *title* and *author* property through the `set` method.
* It allows creating new books that are added to the collection with the `new` method.
* It allows deleting existing books from the collection with the `delete` method.
Expand Down Expand Up @@ -42,17 +42,17 @@ Run the client on two separate devices. Disconnect one device, then make changes

### Get book collection
```
GET http://localhost:8080/api/bookService/books
GET http://localhost:8080/api/library/books
```

### Get book
```
GET http://localhost:8080/api/bookService/book/<BOOK_ID>
GET http://localhost:8080/api/library/book/<BOOK_ID>
```

### Update book properties
```
POST http://localhost:8080/api/bookService/book/<BOOK_ID>/set
POST http://localhost:8080/api/library/book/<BOOK_ID>/set
```
*Body*
```
Expand All @@ -61,7 +61,7 @@ POST http://localhost:8080/api/bookService/book/<BOOK_ID>/set

### Add new book
```
POST http://localhost:8080/api/bookService/books/add
POST http://localhost:8080/api/library/books/add
```
*Body*
```
Expand All @@ -70,7 +70,7 @@ POST http://localhost:8080/api/bookService/books/add

### Delete book
```
POST http://localhost:8080/api/bookService/books/delete
POST http://localhost:8080/api/library/books/delete
```
*Body*
```
Expand Down
6 changes: 3 additions & 3 deletions examples/edit-text/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions examples/hello-world/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions server/apiHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func (s *Service) initAPIHandler() error {
// setCommonHeaders sets common headers such as Access-Control-*.
// It returns error if the origin header does not match any allowed origin.
func (s *Service) setCommonHeaders(w http.ResponseWriter, r *http.Request) error {
if s.cfg.HeaderAuth != nil {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if s.cfg.allowOrigin[0] == "*" {
w.Header().Set("Access-Control-Allow-Origin", "*")
return nil
Expand Down
34 changes: 22 additions & 12 deletions server/codec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import (
)

var (
noQueryGetRequest = []byte(`{}`)
errMissingResult = reserr.InternalError(errors.New("response missing result"))
errInvalidResponse = reserr.InternalError(errors.New("invalid service response"))
errInvalidValue = reserr.InternalError(errors.New("invalid value"))
noQueryGetRequest = []byte(`{}`)
errMissingResult = reserr.InternalError(errors.New("response missing result"))
errInvalidResponse = reserr.InternalError(errors.New("invalid service response"))
errInvalidValue = reserr.InternalError(errors.New("invalid value"))
errInvalidValueEmptyRID = reserr.InternalError(errors.New(`invalid value: resource references requires a non-empty "rid" value`))
errInvalidValueAmbiguous = reserr.InternalError(errors.New(`invalid value: ambiguous value type`))
errInvalidValueObjectNotAllowed = reserr.InternalError(errors.New(`invalid value: nested json object must be wrapped as a data value`))
errInvalidValueArrayNotAllowed = reserr.InternalError(errors.New(`invalid value: nested json array must be wrapped as a data value`))
)

const (
Expand Down Expand Up @@ -239,13 +243,16 @@ func (v *Value) UnmarshalJSON(data []byte) error {

switch {
case mvo.RID != nil:
// Invalid to have both RID and Action or Data set, or if RID is empty
if mvo.Action != nil || mvo.Data != nil || *mvo.RID == "" {
return errInvalidValue
if *mvo.RID == "" {
return errInvalidValueEmptyRID
}
// Invalid to have both RID and Action or Data set
if mvo.Action != nil || mvo.Data != nil {
return errInvalidValueAmbiguous
}
v.RID = *mvo.RID
if !IsValidRID(v.RID, true) {
return errInvalidValue
return reserr.InternalError(errors.New(`invalid value: resource reference rid "` + v.RID + `" is invalid`))
}
if mvo.Soft {
v.Type = ValueTypeSoftReference
Expand All @@ -254,8 +261,11 @@ func (v *Value) UnmarshalJSON(data []byte) error {
}
case mvo.Action != nil:
// Invalid to have both Action and Data set, or if action is not actionDelete
if mvo.Data != nil || *mvo.Action != actionDelete {
return errInvalidValue
if mvo.Data != nil {
return errInvalidValueAmbiguous
}
if *mvo.Action != actionDelete {
return reserr.InternalError(errors.New(`invalid value: unknown action "` + *mvo.Action + `"`))
}
v.Type = ValueTypeDelete
case mvo.Data != nil:
Expand All @@ -269,10 +279,10 @@ func (v *Value) UnmarshalJSON(data []byte) error {
v.Type = ValueTypePrimitive
}
default:
return errInvalidValue
return errInvalidValueObjectNotAllowed
}
case '[':
return errInvalidValue
return errInvalidValueArrayNotAllowed
default:
v.Type = ValueTypePrimitive
}
Expand Down
2 changes: 1 addition & 1 deletion server/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "time"

const (
// Version is the current version for the server.
Version = "1.6.1"
Version = "1.6.2"

// ProtocolVersion is the implemented RES protocol version.
ProtocolVersion = "1.2.1"
Expand Down
4 changes: 1 addition & 3 deletions server/wsConn.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,8 @@ func (s *Service) newWSConn(ws *websocket.Conn, request *http.Request, protocol
return nil
}

cid := xid.New()

conn := &wsConn{
cid: cid.String(),
cid: xid.New().String(),
ws: ws,
request: request,
serv: s,
Expand Down
87 changes: 83 additions & 4 deletions test/14http_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@ func TestHTTPGet_AllowOrigin_ExpectedResponse(t *testing.T) {
ExpectedMissingHeaders []string // Expected response headers not to be included
ExpectedBody interface{} // Expected response body
}{
{"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse},
{"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, successResponse},
{"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, nil, successResponse},
{"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary", "Access-Control-Allow-Credentials"}, successResponse},
{"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse},
{"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse},
// Invalid requests
{"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, reserr.ErrForbiddenOrigin},
{"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, reserr.ErrForbiddenOrigin},
// No Origin header in request
{"", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse},
{"", "", "http://localhost", http.StatusOK, nil, []string{"Access-Control-Allow-Origin", "Vary"}, successResponse},
Expand Down Expand Up @@ -300,3 +300,82 @@ func TestHTTPGet_AllowOrigin_ExpectedResponse(t *testing.T) {
})
}
}

func TestHTTPGet_HeaderAuth_ExpectedResponse(t *testing.T) {
model := resourceData("test.model")
token := json.RawMessage(`{"user":"foo"}`)
successResponse := json.RawMessage(model)

tbl := []struct {
AuthResponse interface{} // Response on auth request. requestTimeout means timeout.
Token interface{} // Token to send. noToken means no token events should be sent.
ExpectedHeaders map[string]string // Expected response Headers
}{
// Without token
{requestTimeout, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{reserr.ErrNotFound, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{[]byte(`{]`), noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{nil, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
// With token
{requestTimeout, token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{reserr.ErrNotFound, token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{[]byte(`{]`), token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{nil, token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
// With nil token
{requestTimeout, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{reserr.ErrNotFound, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{[]byte(`{]`), nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{nil, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
}

for i, l := range tbl {
l := l
runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) {
hreq := s.HTTPRequest("GET", "/api/test/model", nil, func(req *http.Request) {
req.Header.Set("Origin", "example.com")
})

req := s.GetRequest(t)
req.AssertSubject(t, "auth.vault.method")
req.AssertPathPayload(t, "header.Origin", []string{"example.com"})
// Send token
expectedToken := l.Token
if l.Token != noToken {
cid := req.PathPayload(t, "cid").(string)
s.ConnEvent(cid, "token", struct {
Token interface{} `json:"token"`
}{l.Token})
} else {
expectedToken = nil
}
// Respond to auth request
if l.AuthResponse == requestTimeout {
req.Timeout()
} else if err, ok := l.AuthResponse.(*reserr.Error); ok {
req.RespondError(err)
} else if raw, ok := l.AuthResponse.([]byte); ok {
req.RespondRaw(raw)
} else {
req.RespondSuccess(l.AuthResponse)
}

// Handle model get and access request
mreqs := s.GetParallelRequests(t, 2)
mreqs.
GetRequest(t, "access.test.model").
AssertPathPayload(t, "token", expectedToken).
RespondSuccess(json.RawMessage(`{"get":true}`))
mreqs.
GetRequest(t, "get.test.model").
RespondSuccess(json.RawMessage(`{"model":` + model + `}`))

// Validate http response
hreq.GetResponse(t).
Equals(t, http.StatusOK, successResponse).
AssertHeaders(t, l.ExpectedHeaders)
}, func(cfg *server.Config) {
headerAuth := "vault.method"
cfg.HeaderAuth = &headerAuth
})
}
}
85 changes: 81 additions & 4 deletions test/15http_post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,11 @@ func TestHTTPPost_AllowOrigin_ExpectedResponse(t *testing.T) {
ExpectedMissingHeaders []string // Expected response headers not to be included
ExpectedBody interface{} // Expected response body
}{
{"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse},
{"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, successResponse},
{"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, nil, successResponse},
{"http://localhost", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary", "Access-Control-Allow-Credentials"}, successResponse},
{"http://localhost", "", "http://localhost", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse},
{"https://resgate.io", "", "http://localhost;https://resgate.io", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "https://resgate.io", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, successResponse},
// Invalid requests
{"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, nil, reserr.ErrForbiddenOrigin},
{"http://example.com", "", "http://localhost;https://resgate.io", http.StatusForbidden, map[string]string{"Access-Control-Allow-Origin": "http://localhost", "Vary": "Origin"}, []string{"Access-Control-Allow-Credentials"}, reserr.ErrForbiddenOrigin},
// No Origin header in request
{"", "", "*", http.StatusOK, map[string]string{"Access-Control-Allow-Origin": "*"}, []string{"Vary"}, successResponse},
{"", "", "http://localhost", http.StatusOK, nil, []string{"Access-Control-Allow-Origin", "Vary"}, successResponse},
Expand Down Expand Up @@ -311,3 +311,80 @@ func TestHTTPPost_AllowOrigin_ExpectedResponse(t *testing.T) {
})
}
}

func TestHTTPPost_HeaderAuth_ExpectedResponse(t *testing.T) {
token := json.RawMessage(`{"user":"foo"}`)
successResponse := json.RawMessage(`{"foo":"bar"}`)

tbl := []struct {
AuthResponse interface{} // Response on auth request. requestTimeout means timeout.
Token interface{} // Token to send. noToken means no token events should be sent.
ExpectedHeaders map[string]string // Expected response Headers
}{
// Without token
{requestTimeout, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{reserr.ErrNotFound, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{[]byte(`{]`), noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{nil, noToken, map[string]string{"Access-Control-Allow-Credentials": "true"}},
// With token
{requestTimeout, token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{reserr.ErrNotFound, token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{[]byte(`{]`), token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{nil, token, map[string]string{"Access-Control-Allow-Credentials": "true"}},
// With nil token
{requestTimeout, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{reserr.ErrNotFound, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{[]byte(`{]`), nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
{nil, nil, map[string]string{"Access-Control-Allow-Credentials": "true"}},
}

for i, l := range tbl {
l := l
runNamedTest(t, fmt.Sprintf("#%d", i+1), func(s *Session) {
hreq := s.HTTPRequest("POST", "/api/test/model/method", nil, func(req *http.Request) {
req.Header.Set("Origin", "example.com")
})

req := s.GetRequest(t)
req.AssertSubject(t, "auth.vault.method")
req.AssertPathPayload(t, "header.Origin", []string{"example.com"})
// Send token
expectedToken := l.Token
if l.Token != noToken {
cid := req.PathPayload(t, "cid").(string)
s.ConnEvent(cid, "token", struct {
Token interface{} `json:"token"`
}{l.Token})
} else {
expectedToken = nil
}
// Respond to auth request
if l.AuthResponse == requestTimeout {
req.Timeout()
} else if err, ok := l.AuthResponse.(*reserr.Error); ok {
req.RespondError(err)
} else if raw, ok := l.AuthResponse.([]byte); ok {
req.RespondRaw(raw)
} else {
req.RespondSuccess(l.AuthResponse)
}

// Handle model get and access request
s.GetRequest(t).
AssertSubject(t, "access.test.model").
AssertPathPayload(t, "token", expectedToken).
RespondSuccess(json.RawMessage(`{"get":true,"call":"*"}`))
s.GetRequest(t).
AssertSubject(t, "call.test.model.method").
RespondSuccess(successResponse)

// Validate http response
hreq.GetResponse(t).
Equals(t, http.StatusOK, successResponse).
AssertHeaders(t, l.ExpectedHeaders)
}, func(cfg *server.Config) {
headerAuth := "vault.method"
cfg.HeaderAuth = &headerAuth
})
}
}
Loading

0 comments on commit 0c95d4a

Please sign in to comment.