add k8s client
This commit is contained in:
5
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/doc.go
generated
vendored
Normal file
5
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/doc.go
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
Package sockjs is a server side implementation of sockjs protocol.
|
||||
*/
|
||||
|
||||
package sockjs
|
||||
32
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/eventsource.go
generated
vendored
Normal file
32
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/eventsource.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *handler) eventSource(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("content-type", "text/event-stream; charset=UTF-8")
|
||||
fmt.Fprintf(rw, "\r\n")
|
||||
rw.(http.Flusher).Flush()
|
||||
|
||||
recv := newHTTPReceiver(rw, h.options.ResponseLimit, new(eventSourceFrameWriter))
|
||||
sess, _ := h.sessionByRequest(req)
|
||||
if err := sess.attachReceiver(recv); err != nil {
|
||||
recv.sendFrame(cFrame)
|
||||
recv.close()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-recv.doneNotify():
|
||||
case <-recv.interruptedNotify():
|
||||
}
|
||||
}
|
||||
|
||||
type eventSourceFrameWriter struct{}
|
||||
|
||||
func (*eventSourceFrameWriter) write(w io.Writer, frame string) (int, error) {
|
||||
return fmt.Fprintf(w, "data: %s\r\n\r\n", frame)
|
||||
}
|
||||
11
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/frame.go
generated
vendored
Normal file
11
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/frame.go
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func closeFrame(status uint32, reason string) string {
|
||||
bytes, _ := json.Marshal([]interface{}{status, reason})
|
||||
return fmt.Sprintf("c%s", string(bytes))
|
||||
}
|
||||
133
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/handler.go
generated
vendored
Normal file
133
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/handler.go
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
prefixRegexp = make(map[string]*regexp.Regexp)
|
||||
prefixRegexpMu sync.Mutex // protects prefixRegexp
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
prefix string
|
||||
options Options
|
||||
handlerFunc func(Session)
|
||||
mappings []*mapping
|
||||
|
||||
sessionsMux sync.Mutex
|
||||
sessions map[string]*session
|
||||
}
|
||||
|
||||
// NewHandler creates new HTTP handler that conforms to the basic net/http.Handler interface.
|
||||
// It takes path prefix, options and sockjs handler function as parameters
|
||||
func NewHandler(prefix string, opts Options, handleFunc func(Session)) http.Handler {
|
||||
return newHandler(prefix, opts, handleFunc)
|
||||
}
|
||||
|
||||
func newHandler(prefix string, opts Options, handlerFunc func(Session)) *handler {
|
||||
h := &handler{
|
||||
prefix: prefix,
|
||||
options: opts,
|
||||
handlerFunc: handlerFunc,
|
||||
sessions: make(map[string]*session),
|
||||
}
|
||||
|
||||
sessionPrefix := prefix + "/[^/.]+/[^/.]+"
|
||||
h.mappings = []*mapping{
|
||||
newMapping("GET", prefix+"[/]?$", welcomeHandler),
|
||||
newMapping("OPTIONS", prefix+"/info$", opts.cookie, xhrCors, cacheFor, opts.info),
|
||||
newMapping("GET", prefix+"/info$", xhrCors, noCache, opts.info),
|
||||
// XHR
|
||||
newMapping("POST", sessionPrefix+"/xhr_send$", opts.cookie, xhrCors, noCache, h.xhrSend),
|
||||
newMapping("OPTIONS", sessionPrefix+"/xhr_send$", opts.cookie, xhrCors, cacheFor, xhrOptions),
|
||||
newMapping("POST", sessionPrefix+"/xhr$", opts.cookie, xhrCors, noCache, h.xhrPoll),
|
||||
newMapping("OPTIONS", sessionPrefix+"/xhr$", opts.cookie, xhrCors, cacheFor, xhrOptions),
|
||||
newMapping("POST", sessionPrefix+"/xhr_streaming$", opts.cookie, xhrCors, noCache, h.xhrStreaming),
|
||||
newMapping("OPTIONS", sessionPrefix+"/xhr_streaming$", opts.cookie, xhrCors, cacheFor, xhrOptions),
|
||||
// EventStream
|
||||
newMapping("GET", sessionPrefix+"/eventsource$", opts.cookie, xhrCors, noCache, h.eventSource),
|
||||
// Htmlfile
|
||||
newMapping("GET", sessionPrefix+"/htmlfile$", opts.cookie, xhrCors, noCache, h.htmlFile),
|
||||
// JsonP
|
||||
newMapping("GET", sessionPrefix+"/jsonp$", opts.cookie, xhrCors, noCache, h.jsonp),
|
||||
newMapping("OPTIONS", sessionPrefix+"/jsonp$", opts.cookie, xhrCors, cacheFor, xhrOptions),
|
||||
newMapping("POST", sessionPrefix+"/jsonp_send$", opts.cookie, xhrCors, noCache, h.jsonpSend),
|
||||
// IFrame
|
||||
newMapping("GET", prefix+"/iframe[0-9-.a-z_]*.html$", cacheFor, h.iframe),
|
||||
}
|
||||
if opts.Websocket {
|
||||
h.mappings = append(h.mappings, newMapping("GET", sessionPrefix+"/websocket$", h.sockjsWebsocket))
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *handler) Prefix() string { return h.prefix }
|
||||
|
||||
func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// iterate over mappings
|
||||
allowedMethods := []string{}
|
||||
for _, mapping := range h.mappings {
|
||||
if match, method := mapping.matches(req); match == fullMatch {
|
||||
for _, hf := range mapping.chain {
|
||||
hf(rw, req)
|
||||
}
|
||||
return
|
||||
} else if match == pathMatch {
|
||||
allowedMethods = append(allowedMethods, method)
|
||||
}
|
||||
}
|
||||
if len(allowedMethods) > 0 {
|
||||
rw.Header().Set("allow", strings.Join(allowedMethods, ", "))
|
||||
rw.Header().Set("Content-Type", "")
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
http.NotFound(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) parseSessionID(url *url.URL) (string, error) {
|
||||
// cache compiled regexp objects for most used prefixes
|
||||
prefixRegexpMu.Lock()
|
||||
session, ok := prefixRegexp[h.prefix]
|
||||
if !ok {
|
||||
session = regexp.MustCompile(h.prefix + "/(?P<server>[^/.]+)/(?P<session>[^/.]+)/.*")
|
||||
prefixRegexp[h.prefix] = session
|
||||
}
|
||||
prefixRegexpMu.Unlock()
|
||||
|
||||
matches := session.FindStringSubmatch(url.Path)
|
||||
if len(matches) == 3 {
|
||||
return matches[2], nil
|
||||
}
|
||||
return "", errors.New("unable to parse URL for session")
|
||||
}
|
||||
|
||||
func (h *handler) sessionByRequest(req *http.Request) (*session, error) {
|
||||
h.sessionsMux.Lock()
|
||||
defer h.sessionsMux.Unlock()
|
||||
sessionID, err := h.parseSessionID(req.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sess, exists := h.sessions[sessionID]
|
||||
if !exists {
|
||||
sess = newSession(sessionID, h.options.DisconnectDelay, h.options.HeartbeatDelay)
|
||||
h.sessions[sessionID] = sess
|
||||
if h.handlerFunc != nil {
|
||||
go h.handlerFunc(sess)
|
||||
}
|
||||
go func() {
|
||||
<-sess.closedNotify()
|
||||
h.sessionsMux.Lock()
|
||||
delete(h.sessions, sessionID)
|
||||
h.sessionsMux.Unlock()
|
||||
}()
|
||||
}
|
||||
return sess, nil
|
||||
}
|
||||
58
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/htmlfile.go
generated
vendored
Normal file
58
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/htmlfile.go
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var iframeTemplate = `<!doctype html>
|
||||
<html><head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
</head><body><h2>Don't panic!</h2>
|
||||
<script>
|
||||
document.domain = document.domain;
|
||||
var c = parent.%s;
|
||||
c.start();
|
||||
function p(d) {c.message(d);};
|
||||
window.onload = function() {c.stop();};
|
||||
</script>
|
||||
`
|
||||
|
||||
func init() {
|
||||
iframeTemplate += strings.Repeat(" ", 1024-len(iframeTemplate)+14)
|
||||
iframeTemplate += "\r\n\r\n"
|
||||
}
|
||||
|
||||
func (h *handler) htmlFile(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("content-type", "text/html; charset=UTF-8")
|
||||
|
||||
req.ParseForm()
|
||||
callback := req.Form.Get("c")
|
||||
if callback == "" {
|
||||
http.Error(rw, `"callback" parameter required`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(rw, iframeTemplate, callback)
|
||||
rw.(http.Flusher).Flush()
|
||||
sess, _ := h.sessionByRequest(req)
|
||||
recv := newHTTPReceiver(rw, h.options.ResponseLimit, new(htmlfileFrameWriter))
|
||||
if err := sess.attachReceiver(recv); err != nil {
|
||||
recv.sendFrame(cFrame)
|
||||
recv.close()
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-recv.doneNotify():
|
||||
case <-recv.interruptedNotify():
|
||||
}
|
||||
}
|
||||
|
||||
type htmlfileFrameWriter struct{}
|
||||
|
||||
func (*htmlfileFrameWriter) write(w io.Writer, frame string) (int, error) {
|
||||
return fmt.Fprintf(w, "<script>\np(%s);\n</script>\r\n", quote(frame))
|
||||
}
|
||||
105
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/httpreceiver.go
generated
vendored
Normal file
105
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/httpreceiver.go
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type frameWriter interface {
|
||||
write(writer io.Writer, frame string) (int, error)
|
||||
}
|
||||
|
||||
type httpReceiverState int
|
||||
|
||||
const (
|
||||
stateHTTPReceiverActive httpReceiverState = iota
|
||||
stateHTTPReceiverClosed
|
||||
)
|
||||
|
||||
type httpReceiver struct {
|
||||
sync.Mutex
|
||||
state httpReceiverState
|
||||
|
||||
frameWriter frameWriter
|
||||
rw http.ResponseWriter
|
||||
maxResponseSize uint32
|
||||
currentResponseSize uint32
|
||||
doneCh chan struct{}
|
||||
interruptCh chan struct{}
|
||||
}
|
||||
|
||||
func newHTTPReceiver(rw http.ResponseWriter, maxResponse uint32, frameWriter frameWriter) *httpReceiver {
|
||||
recv := &httpReceiver{
|
||||
rw: rw,
|
||||
frameWriter: frameWriter,
|
||||
maxResponseSize: maxResponse,
|
||||
doneCh: make(chan struct{}),
|
||||
interruptCh: make(chan struct{}),
|
||||
}
|
||||
if closeNotifier, ok := rw.(http.CloseNotifier); ok {
|
||||
// if supported check for close notifications from http.RW
|
||||
closeNotifyCh := closeNotifier.CloseNotify()
|
||||
go func() {
|
||||
select {
|
||||
case <-closeNotifyCh:
|
||||
recv.Lock()
|
||||
defer recv.Unlock()
|
||||
if recv.state < stateHTTPReceiverClosed {
|
||||
recv.state = stateHTTPReceiverClosed
|
||||
close(recv.interruptCh)
|
||||
}
|
||||
case <-recv.doneCh:
|
||||
// ok, no action needed here, receiver closed in correct way
|
||||
// just finish the routine
|
||||
}
|
||||
}()
|
||||
}
|
||||
return recv
|
||||
}
|
||||
|
||||
func (recv *httpReceiver) sendBulk(messages ...string) {
|
||||
if len(messages) > 0 {
|
||||
recv.sendFrame(fmt.Sprintf("a[%s]",
|
||||
strings.Join(
|
||||
transform(messages, quote),
|
||||
",",
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func (recv *httpReceiver) sendFrame(value string) {
|
||||
recv.Lock()
|
||||
defer recv.Unlock()
|
||||
|
||||
if recv.state == stateHTTPReceiverActive {
|
||||
// TODO(igm) check err, possibly act as if interrupted
|
||||
n, _ := recv.frameWriter.write(recv.rw, value)
|
||||
recv.currentResponseSize += uint32(n)
|
||||
if recv.currentResponseSize >= recv.maxResponseSize {
|
||||
recv.state = stateHTTPReceiverClosed
|
||||
close(recv.doneCh)
|
||||
} else {
|
||||
recv.rw.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (recv *httpReceiver) doneNotify() <-chan struct{} { return recv.doneCh }
|
||||
func (recv *httpReceiver) interruptedNotify() <-chan struct{} { return recv.interruptCh }
|
||||
func (recv *httpReceiver) close() {
|
||||
recv.Lock()
|
||||
defer recv.Unlock()
|
||||
if recv.state < stateHTTPReceiverClosed {
|
||||
recv.state = stateHTTPReceiverClosed
|
||||
close(recv.doneCh)
|
||||
}
|
||||
}
|
||||
func (recv *httpReceiver) canSend() bool {
|
||||
recv.Lock()
|
||||
defer recv.Unlock()
|
||||
return recv.state != stateHTTPReceiverClosed
|
||||
}
|
||||
42
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/iframe.go
generated
vendored
Normal file
42
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/iframe.go
generated
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var tmpl = template.Must(template.New("iframe").Parse(iframeBody))
|
||||
|
||||
func (h *handler) iframe(rw http.ResponseWriter, req *http.Request) {
|
||||
etagReq := req.Header.Get("If-None-Match")
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(iframeBody))
|
||||
etag := fmt.Sprintf("%x", hash.Sum(nil))
|
||||
if etag == etagReq {
|
||||
rw.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
rw.Header().Add("ETag", etag)
|
||||
tmpl.Execute(rw, h.options.SockJSURL)
|
||||
}
|
||||
|
||||
var iframeBody = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<script>
|
||||
document.domain = document.domain;
|
||||
_sockjs_onload = function(){SockJS.bootstrap_iframe();};
|
||||
</script>
|
||||
<script src="{{.}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Don't panic!</h2>
|
||||
<p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>
|
||||
</body>
|
||||
</html>`
|
||||
77
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/jsonp.go
generated
vendored
Normal file
77
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/jsonp.go
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (h *handler) jsonp(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("content-type", "application/javascript; charset=UTF-8")
|
||||
|
||||
req.ParseForm()
|
||||
callback := req.Form.Get("c")
|
||||
if callback == "" {
|
||||
http.Error(rw, `"callback" parameter required`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
rw.(http.Flusher).Flush()
|
||||
|
||||
sess, _ := h.sessionByRequest(req)
|
||||
recv := newHTTPReceiver(rw, 1, &jsonpFrameWriter{callback})
|
||||
if err := sess.attachReceiver(recv); err != nil {
|
||||
recv.sendFrame(cFrame)
|
||||
recv.close()
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-recv.doneNotify():
|
||||
case <-recv.interruptedNotify():
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) jsonpSend(rw http.ResponseWriter, req *http.Request) {
|
||||
req.ParseForm()
|
||||
var data io.Reader
|
||||
data = req.Body
|
||||
|
||||
formReader := strings.NewReader(req.PostFormValue("d"))
|
||||
if formReader.Len() != 0 {
|
||||
data = formReader
|
||||
}
|
||||
if data == nil {
|
||||
http.Error(rw, "Payload expected.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var messages []string
|
||||
err := json.NewDecoder(data).Decode(&messages)
|
||||
if err == io.EOF {
|
||||
http.Error(rw, "Payload expected.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(rw, "Broken JSON encoding.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sessionID, _ := h.parseSessionID(req.URL)
|
||||
h.sessionsMux.Lock()
|
||||
defer h.sessionsMux.Unlock()
|
||||
if sess, ok := h.sessions[sessionID]; !ok {
|
||||
http.NotFound(rw, req)
|
||||
} else {
|
||||
_ = sess.accept(messages...) // TODO(igm) reponse with http.StatusInternalServerError in case of err?
|
||||
rw.Header().Set("content-type", "text/plain; charset=UTF-8")
|
||||
rw.Write([]byte("ok"))
|
||||
}
|
||||
}
|
||||
|
||||
type jsonpFrameWriter struct {
|
||||
callback string
|
||||
}
|
||||
|
||||
func (j *jsonpFrameWriter) write(w io.Writer, frame string) (int, error) {
|
||||
return fmt.Fprintf(w, "%s(%s);\r\n", j.callback, quote(frame))
|
||||
}
|
||||
36
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/mapping.go
generated
vendored
Normal file
36
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/mapping.go
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type mapping struct {
|
||||
method string
|
||||
path *regexp.Regexp
|
||||
chain []http.HandlerFunc
|
||||
}
|
||||
|
||||
func newMapping(method string, re string, handlers ...http.HandlerFunc) *mapping {
|
||||
return &mapping{method, regexp.MustCompile(re), handlers}
|
||||
}
|
||||
|
||||
type matchType uint32
|
||||
|
||||
const (
|
||||
fullMatch matchType = iota
|
||||
pathMatch
|
||||
noMatch
|
||||
)
|
||||
|
||||
// matches checks if given req.URL is a match with a mapping. Match can be either full, partial (http method mismatch) or no match.
|
||||
func (m *mapping) matches(req *http.Request) (match matchType, method string) {
|
||||
if !m.path.MatchString(req.URL.Path) {
|
||||
match, method = noMatch, ""
|
||||
} else if m.method != req.Method {
|
||||
match, method = pathMatch, m.method
|
||||
} else {
|
||||
match, method = fullMatch, m.method
|
||||
}
|
||||
return
|
||||
}
|
||||
114
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/options.go
generated
vendored
Normal file
114
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/options.go
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
entropy *rand.Rand
|
||||
entropyMutex sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
entropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
// Options type is used for defining various sockjs options
|
||||
type Options struct {
|
||||
// Transports which don't support cross-domain communication natively ('eventsource' to name one) use an iframe trick.
|
||||
// A simple page is served from the SockJS server (using its foreign domain) and is placed in an invisible iframe.
|
||||
// Code run from this iframe doesn't need to worry about cross-domain issues, as it's being run from domain local to the SockJS server.
|
||||
// This iframe also does need to load SockJS javascript client library, and this option lets you specify its url (if you're unsure,
|
||||
// point it to the latest minified SockJS client release, this is the default). You must explicitly specify this url on the server
|
||||
// side for security reasons - we don't want the possibility of running any foreign javascript within the SockJS domain (aka cross site scripting attack).
|
||||
// Also, sockjs javascript library is probably already cached by the browser - it makes sense to reuse the sockjs url you're using in normally.
|
||||
SockJSURL string
|
||||
// Most streaming transports save responses on the client side and don't free memory used by delivered messages.
|
||||
// Such transports need to be garbage-collected once in a while. `response_limit` sets a minimum number of bytes that can be send
|
||||
// over a single http streaming request before it will be closed. After that client needs to open new request.
|
||||
// Setting this value to one effectively disables streaming and will make streaming transports to behave like polling transports.
|
||||
// The default value is 128K.
|
||||
ResponseLimit uint32
|
||||
// Some load balancers don't support websockets. This option can be used to disable websockets support by the server. By default websockets are enabled.
|
||||
Websocket bool
|
||||
// In order to keep proxies and load balancers from closing long running http requests we need to pretend that the connection is active
|
||||
// and send a heartbeat packet once in a while. This setting controls how often this is done.
|
||||
// By default a heartbeat packet is sent every 25 seconds.
|
||||
HeartbeatDelay time.Duration
|
||||
// The server closes a session when a client receiving connection have not been seen for a while.
|
||||
// This delay is configured by this setting.
|
||||
// By default the session is closed when a receiving connection wasn't seen for 5 seconds.
|
||||
DisconnectDelay time.Duration
|
||||
// Some hosting providers enable sticky sessions only to requests that have JSessionID cookie set.
|
||||
// This setting controls if the server should set this cookie to a dummy value.
|
||||
// By default setting JSessionID cookie is disabled. More sophisticated behaviour can be achieved by supplying a function.
|
||||
JSessionID func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// DefaultOptions is a convenient set of options to be used for sockjs
|
||||
var DefaultOptions = Options{
|
||||
Websocket: true,
|
||||
JSessionID: nil,
|
||||
SockJSURL: "http://cdn.sockjs.org/sockjs-0.3.min.js",
|
||||
HeartbeatDelay: 25 * time.Second,
|
||||
DisconnectDelay: 5 * time.Second,
|
||||
ResponseLimit: 128 * 1024,
|
||||
}
|
||||
|
||||
type info struct {
|
||||
Websocket bool `json:"websocket"`
|
||||
CookieNeeded bool `json:"cookie_needed"`
|
||||
Origins []string `json:"origins"`
|
||||
Entropy int32 `json:"entropy"`
|
||||
}
|
||||
|
||||
func (options *Options) info(rw http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
rw.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
json.NewEncoder(rw).Encode(info{
|
||||
Websocket: options.Websocket,
|
||||
CookieNeeded: options.JSessionID != nil,
|
||||
Origins: []string{"*:*"},
|
||||
Entropy: generateEntropy(),
|
||||
})
|
||||
case "OPTIONS":
|
||||
rw.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET")
|
||||
rw.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", 365*24*60*60))
|
||||
rw.WriteHeader(http.StatusNoContent) // 204
|
||||
default:
|
||||
http.NotFound(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultJSessionID is a default behaviour function to be used in options for JSessionID if JSESSIONID is needed
|
||||
func DefaultJSessionID(rw http.ResponseWriter, req *http.Request) {
|
||||
cookie, err := req.Cookie("JSESSIONID")
|
||||
if err == http.ErrNoCookie {
|
||||
cookie = &http.Cookie{
|
||||
Name: "JSESSIONID",
|
||||
Value: "dummy",
|
||||
}
|
||||
}
|
||||
cookie.Path = "/"
|
||||
header := rw.Header()
|
||||
header.Add("Set-Cookie", cookie.String())
|
||||
}
|
||||
|
||||
func (options *Options) cookie(rw http.ResponseWriter, req *http.Request) {
|
||||
if options.JSessionID != nil { // cookie is needed
|
||||
options.JSessionID(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
func generateEntropy() int32 {
|
||||
entropyMutex.Lock()
|
||||
entropy := entropy.Int31()
|
||||
entropyMutex.Unlock()
|
||||
return entropy
|
||||
}
|
||||
219
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/session.go
generated
vendored
Normal file
219
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/session.go
generated
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type sessionState uint32
|
||||
|
||||
const (
|
||||
// brand new session, need to send "h" to receiver
|
||||
sessionOpening sessionState = iota
|
||||
// active session
|
||||
sessionActive
|
||||
// session being closed, sending "closeFrame" to receivers
|
||||
sessionClosing
|
||||
// closed session, no activity at all, should be removed from handler completely and not reused
|
||||
sessionClosed
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSessionNotOpen error is used to denote session not in open state.
|
||||
// Recv() and Send() operations are not suppored if session is closed.
|
||||
ErrSessionNotOpen = errors.New("sockjs: session not in open state")
|
||||
errSessionReceiverAttached = errors.New("sockjs: another receiver already attached")
|
||||
)
|
||||
|
||||
type session struct {
|
||||
sync.Mutex
|
||||
id string
|
||||
state sessionState
|
||||
// protocol dependent receiver (xhr, eventsource, ...)
|
||||
recv receiver
|
||||
// messages to be sent to client
|
||||
sendBuffer []string
|
||||
// messages received from client to be consumed by application
|
||||
// receivedBuffer chan string
|
||||
msgReader *io.PipeReader
|
||||
msgWriter *io.PipeWriter
|
||||
msgEncoder *gob.Encoder
|
||||
msgDecoder *gob.Decoder
|
||||
|
||||
// closeFrame to send after session is closed
|
||||
closeFrame string
|
||||
|
||||
// internal timer used to handle session expiration if no receiver is attached, or heartbeats if recevier is attached
|
||||
sessionTimeoutInterval time.Duration
|
||||
heartbeatInterval time.Duration
|
||||
timer *time.Timer
|
||||
// once the session timeouts this channel also closes
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
type receiver interface {
|
||||
// sendBulk send multiple data messages in frame frame in format: a["msg 1", "msg 2", ....]
|
||||
sendBulk(...string)
|
||||
// sendFrame sends given frame over the wire (with possible chunking depending on receiver)
|
||||
sendFrame(string)
|
||||
// close closes the receiver in a "done" way (idempotent)
|
||||
close()
|
||||
canSend() bool
|
||||
// done notification channel gets closed whenever receiver ends
|
||||
doneNotify() <-chan struct{}
|
||||
// interrupted channel gets closed whenever receiver is interrupted (i.e. http connection drops,...)
|
||||
interruptedNotify() <-chan struct{}
|
||||
}
|
||||
|
||||
// Session is a central component that handles receiving and sending frames. It maintains internal state
|
||||
func newSession(sessionID string, sessionTimeoutInterval, heartbeatInterval time.Duration) *session {
|
||||
r, w := io.Pipe()
|
||||
s := &session{
|
||||
id: sessionID,
|
||||
msgReader: r,
|
||||
msgWriter: w,
|
||||
msgEncoder: gob.NewEncoder(w),
|
||||
msgDecoder: gob.NewDecoder(r),
|
||||
sessionTimeoutInterval: sessionTimeoutInterval,
|
||||
heartbeatInterval: heartbeatInterval,
|
||||
closeCh: make(chan struct{})}
|
||||
s.Lock() // "go test -race" complains if ommited, not sure why as no race can happen here
|
||||
s.timer = time.AfterFunc(sessionTimeoutInterval, s.close)
|
||||
s.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *session) sendMessage(msg string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.state > sessionActive {
|
||||
return ErrSessionNotOpen
|
||||
}
|
||||
s.sendBuffer = append(s.sendBuffer, msg)
|
||||
if s.recv != nil && s.recv.canSend() {
|
||||
s.recv.sendBulk(s.sendBuffer...)
|
||||
s.sendBuffer = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) attachReceiver(recv receiver) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.recv != nil {
|
||||
return errSessionReceiverAttached
|
||||
}
|
||||
s.recv = recv
|
||||
go func(r receiver) {
|
||||
select {
|
||||
case <-r.doneNotify():
|
||||
s.detachReceiver()
|
||||
case <-r.interruptedNotify():
|
||||
s.detachReceiver()
|
||||
s.close()
|
||||
}
|
||||
}(recv)
|
||||
|
||||
if s.state == sessionClosing {
|
||||
s.recv.sendFrame(s.closeFrame)
|
||||
s.recv.close()
|
||||
return nil
|
||||
}
|
||||
if s.state == sessionOpening {
|
||||
s.recv.sendFrame("o")
|
||||
s.state = sessionActive
|
||||
}
|
||||
s.recv.sendBulk(s.sendBuffer...)
|
||||
s.sendBuffer = nil
|
||||
s.timer.Stop()
|
||||
if s.heartbeatInterval > 0 {
|
||||
s.timer = time.AfterFunc(s.heartbeatInterval, s.heartbeat)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) detachReceiver() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.timer.Stop()
|
||||
s.timer = time.AfterFunc(s.sessionTimeoutInterval, s.close)
|
||||
s.recv = nil
|
||||
}
|
||||
|
||||
func (s *session) heartbeat() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.recv != nil { // timer could have fired between Lock and timer.Stop in detachReceiver
|
||||
s.recv.sendFrame("h")
|
||||
s.timer = time.AfterFunc(s.heartbeatInterval, s.heartbeat)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) accept(messages ...string) error {
|
||||
for _, msg := range messages {
|
||||
if err := s.msgEncoder.Encode(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// idempotent operation
|
||||
func (s *session) closing() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.state < sessionClosing {
|
||||
s.msgReader.Close()
|
||||
s.msgWriter.Close()
|
||||
s.state = sessionClosing
|
||||
if s.recv != nil {
|
||||
s.recv.sendFrame(s.closeFrame)
|
||||
s.recv.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// idempotent operation
|
||||
func (s *session) close() {
|
||||
s.closing()
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.state < sessionClosed {
|
||||
s.state = sessionClosed
|
||||
s.timer.Stop()
|
||||
close(s.closeCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) closedNotify() <-chan struct{} { return s.closeCh }
|
||||
|
||||
// Conn interface implementation
|
||||
func (s *session) Close(status uint32, reason string) error {
|
||||
s.Lock()
|
||||
if s.state < sessionClosing {
|
||||
s.closeFrame = closeFrame(status, reason)
|
||||
s.Unlock()
|
||||
s.closing()
|
||||
return nil
|
||||
}
|
||||
s.Unlock()
|
||||
return ErrSessionNotOpen
|
||||
}
|
||||
|
||||
func (s *session) Recv() (string, error) {
|
||||
var msg string
|
||||
err := s.msgDecoder.Decode(&msg)
|
||||
if err == io.ErrClosedPipe {
|
||||
err = ErrSessionNotOpen
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (s *session) Send(msg string) error {
|
||||
return s.sendMessage(msg)
|
||||
}
|
||||
|
||||
func (s *session) ID() string { return s.id }
|
||||
13
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/sockjs.go
generated
vendored
Normal file
13
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/sockjs.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package sockjs
|
||||
|
||||
// Session represents a connection between server and client.
|
||||
type Session interface {
|
||||
// Id returns a session id
|
||||
ID() string
|
||||
// Recv reads one text frame from session
|
||||
Recv() (string, error)
|
||||
// Send sends one text frame to session
|
||||
Send(string) error
|
||||
// Close closes the session with provided code and reason.
|
||||
Close(status uint32, reason string) error
|
||||
}
|
||||
16
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/utils.go
generated
vendored
Normal file
16
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/utils.go
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package sockjs
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func quote(in string) string {
|
||||
quoted, _ := json.Marshal(in)
|
||||
return string(quoted)
|
||||
}
|
||||
|
||||
func transform(values []string, transformFn func(string) string) []string {
|
||||
ret := make([]string, len(values))
|
||||
for i, msg := range values {
|
||||
ret[i] = transformFn(msg)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
47
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/web.go
generated
vendored
Normal file
47
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/web.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func xhrCors(rw http.ResponseWriter, req *http.Request) {
|
||||
header := rw.Header()
|
||||
origin := req.Header.Get("origin")
|
||||
if origin == "" || origin == "null" {
|
||||
origin = "*"
|
||||
}
|
||||
header.Set("Access-Control-Allow-Origin", origin)
|
||||
|
||||
if allowHeaders := req.Header.Get("Access-Control-Request-Headers"); allowHeaders != "" && allowHeaders != "null" {
|
||||
header.Add("Access-Control-Allow-Headers", allowHeaders)
|
||||
}
|
||||
header.Add("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
func xhrOptions(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("Access-Control-Allow-Methods", "OPTIONS, POST")
|
||||
rw.WriteHeader(http.StatusNoContent) // 204
|
||||
}
|
||||
|
||||
func cacheFor(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", 365*24*60*60))
|
||||
rw.Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(time.RFC1123))
|
||||
rw.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", 365*24*60*60))
|
||||
}
|
||||
|
||||
func noCache(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
}
|
||||
|
||||
func welcomeHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("content-type", "text/plain;charset=UTF-8")
|
||||
fmt.Fprintf(rw, "Welcome to SockJS!\n")
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, error string, code int) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
fmt.Fprintf(w, error)
|
||||
}
|
||||
97
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/websocket.go
generated
vendored
Normal file
97
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/websocket.go
generated
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketReadBufSize is a parameter that is used for WebSocket Upgrader.
|
||||
// https://github.com/gorilla/websocket/blob/master/server.go#L230
|
||||
var WebSocketReadBufSize = 4096
|
||||
|
||||
// WebSocketWriteBufSize is a parameter that is used for WebSocket Upgrader
|
||||
// https://github.com/gorilla/websocket/blob/master/server.go#L230
|
||||
var WebSocketWriteBufSize = 4096
|
||||
|
||||
func (h *handler) sockjsWebsocket(rw http.ResponseWriter, req *http.Request) {
|
||||
conn, err := websocket.Upgrade(rw, req, nil, WebSocketReadBufSize, WebSocketWriteBufSize)
|
||||
if _, ok := err.(websocket.HandshakeError); ok {
|
||||
http.Error(rw, `Can "Upgrade" only to "WebSocket".`, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sessID, _ := h.parseSessionID(req.URL)
|
||||
sess := newSession(sessID, h.options.DisconnectDelay, h.options.HeartbeatDelay)
|
||||
if h.handlerFunc != nil {
|
||||
go h.handlerFunc(sess)
|
||||
}
|
||||
|
||||
receiver := newWsReceiver(conn)
|
||||
sess.attachReceiver(receiver)
|
||||
readCloseCh := make(chan struct{})
|
||||
go func() {
|
||||
var d []string
|
||||
for {
|
||||
err := conn.ReadJSON(&d)
|
||||
if err != nil {
|
||||
close(readCloseCh)
|
||||
return
|
||||
}
|
||||
sess.accept(d...)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-readCloseCh:
|
||||
case <-receiver.doneNotify():
|
||||
}
|
||||
sess.close()
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
type wsReceiver struct {
|
||||
conn *websocket.Conn
|
||||
closeCh chan struct{}
|
||||
}
|
||||
|
||||
func newWsReceiver(conn *websocket.Conn) *wsReceiver {
|
||||
return &wsReceiver{
|
||||
conn: conn,
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wsReceiver) sendBulk(messages ...string) {
|
||||
if len(messages) > 0 {
|
||||
w.sendFrame(fmt.Sprintf("a[%s]", strings.Join(transform(messages, quote), ",")))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wsReceiver) sendFrame(frame string) {
|
||||
if err := w.conn.WriteMessage(websocket.TextMessage, []byte(frame)); err != nil {
|
||||
w.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wsReceiver) close() {
|
||||
select {
|
||||
case <-w.closeCh: // already closed
|
||||
default:
|
||||
close(w.closeCh)
|
||||
}
|
||||
}
|
||||
func (w *wsReceiver) canSend() bool {
|
||||
select {
|
||||
case <-w.closeCh: // already closed
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
func (w *wsReceiver) doneNotify() <-chan struct{} { return w.closeCh }
|
||||
func (w *wsReceiver) interruptedNotify() <-chan struct{} { return nil }
|
||||
88
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/xhr.go
generated
vendored
Normal file
88
vendor/gopkg.in/igm/sockjs-go.v2/sockjs/xhr.go
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
package sockjs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
cFrame = closeFrame(2010, "Another connection still open")
|
||||
xhrStreamingPrelude = strings.Repeat("h", 2048)
|
||||
)
|
||||
|
||||
func (h *handler) xhrSend(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Body == nil {
|
||||
httpError(rw, "Payload expected.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var messages []string
|
||||
err := json.NewDecoder(req.Body).Decode(&messages)
|
||||
if err == io.EOF {
|
||||
httpError(rw, "Payload expected.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, ok := err.(*json.SyntaxError); ok || err == io.ErrUnexpectedEOF {
|
||||
httpError(rw, "Broken JSON encoding.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sessionID, err := h.parseSessionID(req.URL)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.sessionsMux.Lock()
|
||||
defer h.sessionsMux.Unlock()
|
||||
if sess, ok := h.sessions[sessionID]; !ok {
|
||||
http.NotFound(rw, req)
|
||||
} else {
|
||||
_ = sess.accept(messages...) // TODO(igm) reponse with SISE in case of err?
|
||||
rw.Header().Set("content-type", "text/plain; charset=UTF-8") // Ignored by net/http (but protocol test complains), see https://code.google.com/p/go/source/detail?r=902dc062bff8
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
type xhrFrameWriter struct{}
|
||||
|
||||
func (*xhrFrameWriter) write(w io.Writer, frame string) (int, error) {
|
||||
return fmt.Fprintf(w, "%s\n", frame)
|
||||
}
|
||||
|
||||
func (h *handler) xhrPoll(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("content-type", "application/javascript; charset=UTF-8")
|
||||
sess, _ := h.sessionByRequest(req) // TODO(igm) add err handling, although err should not happen as handler should not pass req in that case
|
||||
receiver := newHTTPReceiver(rw, 1, new(xhrFrameWriter))
|
||||
if err := sess.attachReceiver(receiver); err != nil {
|
||||
receiver.sendFrame(cFrame)
|
||||
receiver.close()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-receiver.doneNotify():
|
||||
case <-receiver.interruptedNotify():
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) xhrStreaming(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("content-type", "application/javascript; charset=UTF-8")
|
||||
fmt.Fprintf(rw, "%s\n", xhrStreamingPrelude)
|
||||
rw.(http.Flusher).Flush()
|
||||
|
||||
sess, _ := h.sessionByRequest(req)
|
||||
receiver := newHTTPReceiver(rw, h.options.ResponseLimit, new(xhrFrameWriter))
|
||||
|
||||
if err := sess.attachReceiver(receiver); err != nil {
|
||||
receiver.sendFrame(cFrame)
|
||||
receiver.close()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-receiver.doneNotify():
|
||||
case <-receiver.interruptedNotify():
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user