This change adds a new websocket endpoint for database events. The events
endpoint allows clients to stream events as they happen in GARM. Events
are defined as a structure containning the event type (create, update, delete),
the database entity involved (instances, pools, repos, etc) and the payload
consisting of the object involved in the event. The payload translates
to the types normally returned by the API and can be deserialized as one
of the types present in the params package.
The events endpoint is a websocket endpoint and it accepts filters as
a simple json send over the websocket connection. The filters allows the
user to specify which entities are of interest, and which operations should
be returned. For example, you may be interested in changes made to pools
or runners, in which case you could create a filter that only returns
update operations for pools. Or update and delete operations.
The filters can be defined as:
{
"filters": [
{
"entity_type": "instance",
"operations": ["update", "delete"]
},
{
"entity_type": "pool"
},
],
"send_everything": false
}
This would return only update and delete events for instances and all events
for pools. Alternatively you can ask GARM to send you everything:
{
"send_everything": true
}
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
298 lines
5.8 KiB
Go
298 lines
5.8 KiB
Go
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package websocket
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
|
|
|
func computeAcceptKey(challengeKey string) string {
|
|
h := sha1.New()
|
|
h.Write([]byte(challengeKey))
|
|
h.Write(keyGUID)
|
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func generateChallengeKey() (string, error) {
|
|
p := make([]byte, 16)
|
|
if _, err := io.ReadFull(rand.Reader, p); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(p), nil
|
|
}
|
|
|
|
// Token octets per RFC 2616.
|
|
var isTokenOctet = [256]bool{
|
|
'!': true,
|
|
'#': true,
|
|
'$': true,
|
|
'%': true,
|
|
'&': true,
|
|
'\'': true,
|
|
'*': true,
|
|
'+': true,
|
|
'-': true,
|
|
'.': true,
|
|
'0': true,
|
|
'1': true,
|
|
'2': true,
|
|
'3': true,
|
|
'4': true,
|
|
'5': true,
|
|
'6': true,
|
|
'7': true,
|
|
'8': true,
|
|
'9': true,
|
|
'A': true,
|
|
'B': true,
|
|
'C': true,
|
|
'D': true,
|
|
'E': true,
|
|
'F': true,
|
|
'G': true,
|
|
'H': true,
|
|
'I': true,
|
|
'J': true,
|
|
'K': true,
|
|
'L': true,
|
|
'M': true,
|
|
'N': true,
|
|
'O': true,
|
|
'P': true,
|
|
'Q': true,
|
|
'R': true,
|
|
'S': true,
|
|
'T': true,
|
|
'U': true,
|
|
'W': true,
|
|
'V': true,
|
|
'X': true,
|
|
'Y': true,
|
|
'Z': true,
|
|
'^': true,
|
|
'_': true,
|
|
'`': true,
|
|
'a': true,
|
|
'b': true,
|
|
'c': true,
|
|
'd': true,
|
|
'e': true,
|
|
'f': true,
|
|
'g': true,
|
|
'h': true,
|
|
'i': true,
|
|
'j': true,
|
|
'k': true,
|
|
'l': true,
|
|
'm': true,
|
|
'n': true,
|
|
'o': true,
|
|
'p': true,
|
|
'q': true,
|
|
'r': true,
|
|
's': true,
|
|
't': true,
|
|
'u': true,
|
|
'v': true,
|
|
'w': true,
|
|
'x': true,
|
|
'y': true,
|
|
'z': true,
|
|
'|': true,
|
|
'~': true,
|
|
}
|
|
|
|
// skipSpace returns a slice of the string s with all leading RFC 2616 linear
|
|
// whitespace removed.
|
|
func skipSpace(s string) (rest string) {
|
|
i := 0
|
|
for ; i < len(s); i++ {
|
|
if b := s[i]; b != ' ' && b != '\t' {
|
|
break
|
|
}
|
|
}
|
|
return s[i:]
|
|
}
|
|
|
|
// nextToken returns the leading RFC 2616 token of s and the string following
|
|
// the token.
|
|
func nextToken(s string) (token, rest string) {
|
|
i := 0
|
|
for ; i < len(s); i++ {
|
|
if !isTokenOctet[s[i]] {
|
|
break
|
|
}
|
|
}
|
|
return s[:i], s[i:]
|
|
}
|
|
|
|
// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
|
|
// and the string following the token or quoted string.
|
|
func nextTokenOrQuoted(s string) (value string, rest string) {
|
|
if !strings.HasPrefix(s, "\"") {
|
|
return nextToken(s)
|
|
}
|
|
s = s[1:]
|
|
for i := 0; i < len(s); i++ {
|
|
switch s[i] {
|
|
case '"':
|
|
return s[:i], s[i+1:]
|
|
case '\\':
|
|
p := make([]byte, len(s)-1)
|
|
j := copy(p, s[:i])
|
|
escape := true
|
|
for i = i + 1; i < len(s); i++ {
|
|
b := s[i]
|
|
switch {
|
|
case escape:
|
|
escape = false
|
|
p[j] = b
|
|
j++
|
|
case b == '\\':
|
|
escape = true
|
|
case b == '"':
|
|
return string(p[:j]), s[i+1:]
|
|
default:
|
|
p[j] = b
|
|
j++
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// equalASCIIFold returns true if s is equal to t with ASCII case folding as
|
|
// defined in RFC 4790.
|
|
func equalASCIIFold(s, t string) bool {
|
|
for s != "" && t != "" {
|
|
sr, size := utf8.DecodeRuneInString(s)
|
|
s = s[size:]
|
|
tr, size := utf8.DecodeRuneInString(t)
|
|
t = t[size:]
|
|
if sr == tr {
|
|
continue
|
|
}
|
|
if 'A' <= sr && sr <= 'Z' {
|
|
sr = sr + 'a' - 'A'
|
|
}
|
|
if 'A' <= tr && tr <= 'Z' {
|
|
tr = tr + 'a' - 'A'
|
|
}
|
|
if sr != tr {
|
|
return false
|
|
}
|
|
}
|
|
return s == t
|
|
}
|
|
|
|
// tokenListContainsValue returns true if the 1#token header with the given
|
|
// name contains a token equal to value with ASCII case folding.
|
|
func tokenListContainsValue(header http.Header, name string, value string) bool {
|
|
headers:
|
|
for _, s := range header[name] {
|
|
for {
|
|
var t string
|
|
t, s = nextToken(skipSpace(s))
|
|
if t == "" {
|
|
continue headers
|
|
}
|
|
s = skipSpace(s)
|
|
if s != "" && s[0] != ',' {
|
|
continue headers
|
|
}
|
|
if equalASCIIFold(t, value) {
|
|
return true
|
|
}
|
|
if s == "" {
|
|
continue headers
|
|
}
|
|
s = s[1:]
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseExtensions parses WebSocket extensions from a header.
|
|
func parseExtensions(header http.Header) []map[string]string {
|
|
// From RFC 6455:
|
|
//
|
|
// Sec-WebSocket-Extensions = extension-list
|
|
// extension-list = 1#extension
|
|
// extension = extension-token *( ";" extension-param )
|
|
// extension-token = registered-token
|
|
// registered-token = token
|
|
// extension-param = token [ "=" (token | quoted-string) ]
|
|
// ;When using the quoted-string syntax variant, the value
|
|
// ;after quoted-string unescaping MUST conform to the
|
|
// ;'token' ABNF.
|
|
|
|
var result []map[string]string
|
|
headers:
|
|
for _, s := range header["Sec-Websocket-Extensions"] {
|
|
for {
|
|
var t string
|
|
t, s = nextToken(skipSpace(s))
|
|
if t == "" {
|
|
continue headers
|
|
}
|
|
ext := map[string]string{"": t}
|
|
for {
|
|
s = skipSpace(s)
|
|
if !strings.HasPrefix(s, ";") {
|
|
break
|
|
}
|
|
var k string
|
|
k, s = nextToken(skipSpace(s[1:]))
|
|
if k == "" {
|
|
continue headers
|
|
}
|
|
s = skipSpace(s)
|
|
var v string
|
|
if strings.HasPrefix(s, "=") {
|
|
v, s = nextTokenOrQuoted(skipSpace(s[1:]))
|
|
s = skipSpace(s)
|
|
}
|
|
if s != "" && s[0] != ',' && s[0] != ';' {
|
|
continue headers
|
|
}
|
|
ext[k] = v
|
|
}
|
|
if s != "" && s[0] != ',' {
|
|
continue headers
|
|
}
|
|
result = append(result, ext)
|
|
if s == "" {
|
|
continue headers
|
|
}
|
|
s = s[1:]
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// isValidChallengeKey checks if the argument meets RFC6455 specification.
|
|
func isValidChallengeKey(s string) bool {
|
|
// From RFC6455:
|
|
//
|
|
// A |Sec-WebSocket-Key| header field with a base64-encoded (see
|
|
// Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in
|
|
// length.
|
|
|
|
if s == "" {
|
|
return false
|
|
}
|
|
decoded, err := base64.StdEncoding.DecodeString(s)
|
|
return err == nil && len(decoded) == 16
|
|
}
|