diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index ea6f7b1f8..f22f8a6e8 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -5689,16 +5689,7 @@ ] }, "io.k8s.apimachinery.pkg.apis.meta.v1.Duration": { - "description": "Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json.", - "required": [ - "Duration" - ], - "properties": { - "Duration": { - "type": "integer", - "format": "int64" - } - } + "type": "string" }, "io.k8s.apimachinery.pkg.apis.meta.v1.GroupVersionForDiscovery": { "description": "GroupVersion contains the \"group/version\" and \"version\" string of a version. It is made a struct to keep extensibility.", diff --git a/apis/incidents/v1alpha1/openapi_generated.go b/apis/incidents/v1alpha1/openapi_generated.go index 6c8d2ff68..754381fe2 100644 --- a/apis/incidents/v1alpha1/openapi_generated.go +++ b/apis/incidents/v1alpha1/openapi_generated.go @@ -495,19 +495,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json.", - Properties: map[string]spec.Schema{ - "Duration": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - Required: []string{"Duration"}, + Type: v1.Duration{}.OpenAPISchemaType(), + Format: v1.Duration{}.OpenAPISchemaFormat(), }, }, - Dependencies: []string{}, }, "k8s.io/apimachinery/pkg/apis/meta/v1.ExportOptions": { Schema: spec.Schema{ diff --git a/apis/monitoring/v1alpha1/crds.yaml b/apis/monitoring/v1alpha1/crds.yaml index fc1357b7e..3d9161b5f 100644 --- a/apis/monitoring/v1alpha1/crds.yaml +++ b/apis/monitoring/v1alpha1/crds.yaml @@ -304,28 +304,12 @@ spec: to create. properties: alertInterval: - description: Duration is a wrapper around time.Duration which supports - correct marshaling to YAML and JSON. In particular, it marshals into - strings, which can be used as map keys in json. - properties: - Duration: - format: int64 - type: integer - required: - - Duration + type: string check: description: Icinga CheckCommand name type: string checkInterval: - description: Duration is a wrapper around time.Duration which supports - correct marshaling to YAML and JSON. In particular, it marshals into - strings, which can be used as map keys in json. - properties: - Duration: - format: int64 - type: integer - required: - - Duration + type: string notifierSecretName: description: Secret containing notifier credentials type: string @@ -663,28 +647,12 @@ spec: description: NodeAlertSpec describes the NodeAlert the user wishes to create. properties: alertInterval: - description: Duration is a wrapper around time.Duration which supports - correct marshaling to YAML and JSON. In particular, it marshals into - strings, which can be used as map keys in json. - properties: - Duration: - format: int64 - type: integer - required: - - Duration + type: string check: description: Icinga CheckCommand name type: string checkInterval: - description: Duration is a wrapper around time.Duration which supports - correct marshaling to YAML and JSON. In particular, it marshals into - strings, which can be used as map keys in json. - properties: - Duration: - format: int64 - type: integer - required: - - Duration + type: string nodeName: type: string notifierSecretName: @@ -1026,28 +994,12 @@ spec: description: PodAlertSpec describes the PodAlert the user wishes to create. properties: alertInterval: - description: Duration is a wrapper around time.Duration which supports - correct marshaling to YAML and JSON. In particular, it marshals into - strings, which can be used as map keys in json. - properties: - Duration: - format: int64 - type: integer - required: - - Duration + type: string check: description: Icinga CheckCommand name type: string checkInterval: - description: Duration is a wrapper around time.Duration which supports - correct marshaling to YAML and JSON. In particular, it marshals into - strings, which can be used as map keys in json. - properties: - Duration: - format: int64 - type: integer - required: - - Duration + type: string notifierSecretName: description: Secret containing notifier credentials type: string diff --git a/apis/monitoring/v1alpha1/openapi_generated.go b/apis/monitoring/v1alpha1/openapi_generated.go index b8f7c3490..3c46224af 100644 --- a/apis/monitoring/v1alpha1/openapi_generated.go +++ b/apis/monitoring/v1alpha1/openapi_generated.go @@ -1420,19 +1420,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "Duration is a wrapper around time.Duration which supports correct marshaling to YAML and JSON. In particular, it marshals into strings, which can be used as map keys in json.", - Properties: map[string]spec.Schema{ - "Duration": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - Required: []string{"Duration"}, + Type: v1.Duration{}.OpenAPISchemaType(), + Format: v1.Duration{}.OpenAPISchemaFormat(), }, }, - Dependencies: []string{}, }, "k8s.io/apimachinery/pkg/apis/meta/v1.ExportOptions": { Schema: spec.Schema{ diff --git a/glide.lock b/glide.lock index bf87bed23..9d500869f 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,8 @@ -hash: 20ff2f27426d06dafa5e5ed1abc457fbb8876a28c3fe52dc2a4fcf59f6ca3c97 -updated: 2018-06-01T23:22:50.778724555-07:00 +hash: 443dff86cb84e92abeb82ff096e200591fe19f58a0287c30547e4402bb8c4476 +updated: 2018-06-20T10:28:52.948194808-07:00 imports: - name: bitbucket.org/atlassian/go-stride - version: 8846b835cb45ec8707e6694533a2b6180861cdbe + version: 78855f0a5d80b2683b11f3825a580a87e32a9f71 subpackages: - pkg/stride - name: bitbucket.org/ww/goautoneg @@ -15,7 +15,7 @@ imports: - name: github.com/appscode/envconfig version: 183bbe643a2875eaa69c85627ac6b25813983203 - name: github.com/appscode/go - version: 230e0e4622c682b5d117db8c28a66404ed874ed5 + version: 578091d435e848d79527d8b911fdf1dd5423b227 subpackages: - analytics - context @@ -49,14 +49,14 @@ imports: - unified - webhook - name: github.com/appscode/jsonpatch - version: 58a38a46ddb591a543c6b1003213353e29331c5a + version: 1359b374de83fa14fd58ab10aa8ea4935f84d249 - name: github.com/appscode/kubernetes-webhook-util version: 5a1b68c81878cdb4e07a7b5dbdbb6c88e7f5fa04 subpackages: - admission/v1beta1 - registry/admissionreview/v1beta1 - name: github.com/appscode/kutil - version: 5fbfa91a3f029faeda0520507a7bfe07df3eedbd + version: 85abe6c009de4b732e5963cb1ec82950efd2fdda subpackages: - apiextensions/v1beta1 - core/v1 @@ -86,7 +86,7 @@ imports: subpackages: - quantile - name: github.com/bwmarrin/discordgo - version: 4a33b9bc7c56cfdb9bb244e33e83cb3941fe2bdc + version: 73f6772a2b7cc95e29c462e4f15bf07cbe0d3854 - name: github.com/cenkalti/backoff version: 2ea60e5f094469f9e65adb9cd103795b73ae743e - name: github.com/codegangsta/inject @@ -203,7 +203,7 @@ imports: - name: github.com/evanphx/json-patch version: 944e07253867aacae43c04b2e6a239005443f33a - name: github.com/fsnotify/fsnotify - version: f12c6236fe7b5cf6bcf30e5935d08cb079d78334 + version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 - name: github.com/ghodss/yaml version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee - name: github.com/go-macaron/auth @@ -223,7 +223,7 @@ imports: - name: github.com/go-openapi/jsonreference version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272 - name: github.com/go-openapi/loads - version: a80dea3052f00e5f032e860dd7355cd0cc67e24d + version: 2a2b323bab96e6b1fdee110e57d959322446e9c9 - name: github.com/go-openapi/spec version: 1de3e0542de65ad8d75452a595886fdd0befb363 - name: github.com/go-openapi/swag @@ -242,11 +242,13 @@ imports: - name: github.com/golang/protobuf version: 1643683e1b54a9e88ad26d98f81400c8c9d9f4f9 subpackages: + - jsonpb - proto - protoc-gen-go/descriptor - ptypes - ptypes/any - ptypes/duration + - ptypes/struct - ptypes/timestamp - name: github.com/google/go-cmp version: 5411ab924f9ffa6566244a9e504bc347edacffd3 @@ -279,7 +281,7 @@ imports: - openstack/utils - pagination - name: github.com/gorilla/websocket - version: 6eb6ad425a89d9da7a5549bc6da8f79ba5c17844 + version: 5ed622c449da6d44c3c8329331ff47a9e5844f71 - name: github.com/hashicorp/go-version version: 53932f80ddea12bea96be074f9fb2dc545806aba repo: https://github.com/appscode/go-version.git @@ -297,7 +299,7 @@ imports: - name: github.com/influxdata/influxdb version: 6ac835404e7e64ea7299a6eebcce1ab1ef15fe3c - name: github.com/jaytaylor/html2text - version: 64a82a6d140778896f13303121a49d8cb8007034 + version: 57d518f124b0cf46ea2021f25a01396b3522e6fb - name: github.com/jpillora/go-ogle-analytics version: 14b04e0594ef6a9fd943363b135656f0ec8c9d0e - name: github.com/json-iterator/go @@ -305,7 +307,7 @@ imports: - name: github.com/Knetic/govaluate version: d216395917cc49052c7c7094cf57f09657ca08a8 - name: github.com/mailgun/mailgun-go - version: 369a1fa57ce6fb314e627087b9fff0db5c266c0f + version: 15f93e2afc54c83d7664adcfd233be4e68606a09 - name: github.com/mailru/easyjson version: 2f5df55504ebc322e4d52d34df6a1f5b503bf26d subpackages: @@ -313,17 +315,17 @@ imports: - jlexer - jwriter - name: github.com/mattn/go-runewidth - version: 9e777a8366cce605130a531d2cd6363d07ad7317 + version: ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb - name: github.com/matttproud/golang_protobuf_extensions version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a subpackages: - pbutil - name: github.com/nlopes/slack - version: 8ab4d0b364ef1e9af5d102531da20d5ec902b6c4 + version: 2123bd8e3090415072d6bfdab8a9096dea25ac20 - name: github.com/NYTimes/gziphandler version: 56545f4a5d46df9a6648819d1664c3a03a13ffdb - name: github.com/olekukonko/tablewriter - version: cca8bbc0798408af109aaaa239cbd2634846b340 + version: d4647c9c7a84d847478d890b816b7d8b62b0b279 - name: github.com/onsi/gomega version: 003f63b7f4cff3fc95357005358af2de0f5fe152 subpackages: @@ -372,7 +374,7 @@ imports: - name: github.com/PuerkitoBio/urlesc version: 5bd2802263f21d8788851d5305584c82a5c75d7e - name: github.com/rs/zerolog - version: dabc72c15b2adeeb6abc68bfdb798ebd29ea48a8 + version: 1a88fbfdd0e441d7f77f13647adb40548a8bb59b subpackages: - internal/cbor - internal/json @@ -402,13 +404,13 @@ imports: subpackages: - doc - name: github.com/spf13/pflag - version: 583c0c0531f06d5278b7d917446061adc344b5cd + version: 3ebe029320b2676d667ae88da602a5f854788a8a - name: github.com/ssor/bom version: 6386211fdfcf24c0bfbdaceafd02849ed9a8a509 - name: github.com/StackExchange/wmi version: cdffdb33acae0e14efff2628f9bae377b597840e - name: github.com/tbruyelle/hipchat-go - version: 749fb9e14beb9995f677c101a754393cecb64b0f + version: 35aebc99209aa41498ab4823a83e254037f062fd subpackages: - hipchat - name: github.com/ugorji/go @@ -416,7 +418,7 @@ imports: subpackages: - codec - name: github.com/Unknwon/com - version: 7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520 + version: da59b551951d50441ca26b7a8cd81317f34df87f - name: github.com/vaughan0/go-ini version: a98ad7ee00ec53921f08832bc06ecf7fd600e6a1 - name: github.com/yudai/gojsondiff @@ -582,7 +584,9 @@ imports: - pkg/client/clientset/clientset/scheme - pkg/client/clientset/clientset/typed/apiextensions/v1beta1 - name: k8s.io/apimachinery - version: 302974c03f7e50f16561ba237db776ab93594ef6 + version: 2d462cff08891d952e216b5372464180933fc26c + repo: https://github.com/pharmer/apimachinery.git + vcs: git subpackages: - pkg/api/equality - pkg/api/errors @@ -898,7 +902,7 @@ imports: - util/retry - util/workqueue - name: k8s.io/kube-openapi - version: 8a9b82f00b3a86eac24681da3f9fe6c34c01cea2 + version: bf40560368791a7dddfeea9b3cfcf89b34139f44 subpackages: - pkg/builder - pkg/common diff --git a/glide.yaml b/glide.yaml index 56c33c257..d7da9b672 100644 --- a/glide.yaml +++ b/glide.yaml @@ -53,7 +53,9 @@ import: - package: k8s.io/apiextensions-apiserver version: kubernetes-1.10.0 - package: k8s.io/apimachinery - version: kubernetes-1.10.0 + repo: https://github.com/pharmer/apimachinery.git + vcs: git + version: release-1.10 - package: k8s.io/apiserver repo: https://github.com/tamalsaha/apiserver.git version: k-1.10 diff --git a/vendor/github.com/Unknwon/com/math.go b/vendor/github.com/Unknwon/com/math.go index 99c56b659..62b77e87c 100644 --- a/vendor/github.com/Unknwon/com/math.go +++ b/vendor/github.com/Unknwon/com/math.go @@ -14,12 +14,12 @@ package com -// PowInt is int type of math.Pow function. +// PowInt is int type of math.Pow function. func PowInt(x int, y int) int { if y <= 0 { return 1 } else { - if y % 2 == 0 { + if y%2 == 0 { sqrt := PowInt(x, y/2) return sqrt * sqrt } else { diff --git a/vendor/github.com/appscode/kutil/meta/arguments.go b/vendor/github.com/appscode/kutil/meta/arguments.go index edeb250ad..6f35d08fb 100644 --- a/vendor/github.com/appscode/kutil/meta/arguments.go +++ b/vendor/github.com/appscode/kutil/meta/arguments.go @@ -21,6 +21,8 @@ package meta import ( "fmt" "strings" + + "github.com/golang/glog" ) // BuildArgumentListFromMap takes two string-string maps, one with the base arguments and one with optional override arguments @@ -48,7 +50,7 @@ func ParseArgumentListToMap(arguments []string) map[string]string { // Warn in all other cases, but don't error out. This can happen only if the user has edited the argument list by hand, so they might know what they are doing if err != nil { if i != 0 { - fmt.Printf("[kubeadm] WARNING: The component argument %q could not be parsed correctly. The argument must be of the form %q. Skipping...", arg, "--") + glog.Warningf("WARNING: The component argument %q could not be parsed correctly. The argument must be of the form %q. Skipping...", arg, "--") } continue } diff --git a/vendor/github.com/fsnotify/fsnotify/fsnotify.go b/vendor/github.com/fsnotify/fsnotify/fsnotify.go index d1d39a0eb..190bf0de5 100644 --- a/vendor/github.com/fsnotify/fsnotify/fsnotify.go +++ b/vendor/github.com/fsnotify/fsnotify/fsnotify.go @@ -9,6 +9,7 @@ package fsnotify import ( "bytes" + "errors" "fmt" ) @@ -30,33 +31,36 @@ const ( Chmod ) -// String returns a string representation of the event in the form -// "file: REMOVE|WRITE|..." -func (e Event) String() string { +func (op Op) String() string { // Use a buffer for efficient string concatenation var buffer bytes.Buffer - if e.Op&Create == Create { + if op&Create == Create { buffer.WriteString("|CREATE") } - if e.Op&Remove == Remove { + if op&Remove == Remove { buffer.WriteString("|REMOVE") } - if e.Op&Write == Write { + if op&Write == Write { buffer.WriteString("|WRITE") } - if e.Op&Rename == Rename { + if op&Rename == Rename { buffer.WriteString("|RENAME") } - if e.Op&Chmod == Chmod { + if op&Chmod == Chmod { buffer.WriteString("|CHMOD") } - - // If buffer remains empty, return no event names if buffer.Len() == 0 { - return fmt.Sprintf("%q: ", e.Name) + return "" } + return buffer.String()[1:] // Strip leading pipe +} - // Return a list of event names, with leading pipe character stripped - return fmt.Sprintf("%q: %s", e.Name, buffer.String()[1:]) +// String returns a string representation of the event in the form +// "file: REMOVE|WRITE|..." +func (e Event) String() string { + return fmt.Sprintf("%q: %s", e.Name, e.Op.String()) } + +// Common errors that can be reported by a watcher +var ErrEventOverflow = errors.New("fsnotify queue overflow") diff --git a/vendor/github.com/fsnotify/fsnotify/inotify.go b/vendor/github.com/fsnotify/fsnotify/inotify.go index 9700df55e..d9fd1b88a 100644 --- a/vendor/github.com/fsnotify/fsnotify/inotify.go +++ b/vendor/github.com/fsnotify/fsnotify/inotify.go @@ -24,7 +24,6 @@ type Watcher struct { Events chan Event Errors chan error mu sync.Mutex // Map access - cv *sync.Cond // sync removing on rm_watch with IN_IGNORE fd int poller *fdPoller watches map[string]*watch // Map of inotify watches (key: path) @@ -36,7 +35,7 @@ type Watcher struct { // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events. func NewWatcher() (*Watcher, error) { // Create inotify fd - fd, errno := unix.InotifyInit() + fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC) if fd == -1 { return nil, errno } @@ -56,7 +55,6 @@ func NewWatcher() (*Watcher, error) { done: make(chan struct{}), doneResp: make(chan struct{}), } - w.cv = sync.NewCond(&w.mu) go w.readEvents() return w, nil @@ -103,21 +101,23 @@ func (w *Watcher) Add(name string) error { var flags uint32 = agnosticEvents w.mu.Lock() - watchEntry, found := w.watches[name] - w.mu.Unlock() - if found { - watchEntry.flags |= flags - flags |= unix.IN_MASK_ADD + defer w.mu.Unlock() + watchEntry := w.watches[name] + if watchEntry != nil { + flags |= watchEntry.flags | unix.IN_MASK_ADD } wd, errno := unix.InotifyAddWatch(w.fd, name, flags) if wd == -1 { return errno } - w.mu.Lock() - w.watches[name] = &watch{wd: uint32(wd), flags: flags} - w.paths[wd] = name - w.mu.Unlock() + if watchEntry == nil { + w.watches[name] = &watch{wd: uint32(wd), flags: flags} + w.paths[wd] = name + } else { + watchEntry.wd = uint32(wd) + watchEntry.flags = flags + } return nil } @@ -135,6 +135,13 @@ func (w *Watcher) Remove(name string) error { if !ok { return fmt.Errorf("can't remove non-existent inotify watch for: %s", name) } + + // We successfully removed the watch if InotifyRmWatch doesn't return an + // error, we need to clean up our internal state to ensure it matches + // inotify's kernel state. + delete(w.paths, int(watch.wd)) + delete(w.watches, name) + // inotify_rm_watch will return EINVAL if the file has been deleted; // the inotify will already have been removed. // watches and pathes are deleted in ignoreLinux() implicitly and asynchronously @@ -152,13 +159,6 @@ func (w *Watcher) Remove(name string) error { return errno } - // wait until ignoreLinux() deleting maps - exists := true - for exists { - w.cv.Wait() - _, exists = w.watches[name] - } - return nil } @@ -245,13 +245,31 @@ func (w *Watcher) readEvents() { mask := uint32(raw.Mask) nameLen := uint32(raw.Len) + + if mask&unix.IN_Q_OVERFLOW != 0 { + select { + case w.Errors <- ErrEventOverflow: + case <-w.done: + return + } + } + // If the event happened to the watched directory or the watched file, the kernel // doesn't append the filename to the event, but we would like to always fill the // the "Name" field with a valid filename. We retrieve the path of the watch from // the "paths" map. w.mu.Lock() - name := w.paths[int(raw.Wd)] + name, ok := w.paths[int(raw.Wd)] + // IN_DELETE_SELF occurs when the file/directory being watched is removed. + // This is a sign to clean up the maps, otherwise we are no longer in sync + // with the inotify kernel state which has already deleted the watch + // automatically. + if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { + delete(w.paths, int(raw.Wd)) + delete(w.watches, name) + } w.mu.Unlock() + if nameLen > 0 { // Point "bytes" at the first byte of the filename bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent])) @@ -262,7 +280,7 @@ func (w *Watcher) readEvents() { event := newEvent(name, mask) // Send the events that are not ignored on the events channel - if !event.ignoreLinux(w, raw.Wd, mask) { + if !event.ignoreLinux(mask) { select { case w.Events <- event: case <-w.done: @@ -279,15 +297,9 @@ func (w *Watcher) readEvents() { // Certain types of events can be "ignored" and not sent over the Events // channel. Such as events marked ignore by the kernel, or MODIFY events // against files that do not exist. -func (e *Event) ignoreLinux(w *Watcher, wd int32, mask uint32) bool { +func (e *Event) ignoreLinux(mask uint32) bool { // Ignore anything the inotify API says to ignore if mask&unix.IN_IGNORED == unix.IN_IGNORED { - w.mu.Lock() - defer w.mu.Unlock() - name := w.paths[int(wd)] - delete(w.paths, int(wd)) - delete(w.watches, name) - w.cv.Broadcast() return true } diff --git a/vendor/github.com/fsnotify/fsnotify/kqueue.go b/vendor/github.com/fsnotify/fsnotify/kqueue.go index c2b4acb18..86e76a3d6 100644 --- a/vendor/github.com/fsnotify/fsnotify/kqueue.go +++ b/vendor/github.com/fsnotify/fsnotify/kqueue.go @@ -22,7 +22,7 @@ import ( type Watcher struct { Events chan Event Errors chan error - done chan bool // Channel for sending a "quit message" to the reader goroutine + done chan struct{} // Channel for sending a "quit message" to the reader goroutine kq int // File descriptor (as returned by the kqueue() syscall). @@ -56,7 +56,7 @@ func NewWatcher() (*Watcher, error) { externalWatches: make(map[string]bool), Events: make(chan Event), Errors: make(chan error), - done: make(chan bool), + done: make(chan struct{}), } go w.readEvents() @@ -71,10 +71,8 @@ func (w *Watcher) Close() error { return nil } w.isClosed = true - w.mu.Unlock() // copy paths to remove while locked - w.mu.Lock() var pathsToRemove = make([]string, 0, len(w.watches)) for name := range w.watches { pathsToRemove = append(pathsToRemove, name) @@ -82,15 +80,12 @@ func (w *Watcher) Close() error { w.mu.Unlock() // unlock before calling Remove, which also locks - var err error for _, name := range pathsToRemove { - if e := w.Remove(name); e != nil && err == nil { - err = e - } + w.Remove(name) } - // Send "quit" message to the reader goroutine: - w.done <- true + // send a "quit" message to the reader goroutine + close(w.done) return nil } @@ -266,17 +261,12 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) { func (w *Watcher) readEvents() { eventBuffer := make([]unix.Kevent_t, 10) +loop: for { // See if there is a message on the "done" channel select { case <-w.done: - err := unix.Close(w.kq) - if err != nil { - w.Errors <- err - } - close(w.Events) - close(w.Errors) - return + break loop default: } @@ -284,7 +274,11 @@ func (w *Watcher) readEvents() { kevents, err := read(w.kq, eventBuffer, &keventWaitTime) // EINTR is okay, the syscall was interrupted before timeout expired. if err != nil && err != unix.EINTR { - w.Errors <- err + select { + case w.Errors <- err: + case <-w.done: + break loop + } continue } @@ -319,8 +313,12 @@ func (w *Watcher) readEvents() { if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) { w.sendDirectoryChangeEvents(event.Name) } else { - // Send the event on the Events channel - w.Events <- event + // Send the event on the Events channel. + select { + case w.Events <- event: + case <-w.done: + break loop + } } if event.Op&Remove == Remove { @@ -352,6 +350,18 @@ func (w *Watcher) readEvents() { kevents = kevents[1:] } } + + // cleanup + err := unix.Close(w.kq) + if err != nil { + // only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors. + select { + case w.Errors <- err: + default: + } + } + close(w.Events) + close(w.Errors) } // newEvent returns an platform-independent Event based on kqueue Fflags. @@ -407,7 +417,11 @@ func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { // Get all files files, err := ioutil.ReadDir(dirPath) if err != nil { - w.Errors <- err + select { + case w.Errors <- err: + case <-w.done: + return + } } // Search for new files @@ -428,7 +442,11 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf w.mu.Unlock() if !doesExist { // Send create event - w.Events <- newCreateEvent(filePath) + select { + case w.Events <- newCreateEvent(filePath): + case <-w.done: + return + } } // like watchDirectoryFiles (but without doing another ReadDir) diff --git a/vendor/github.com/go-openapi/loads/spec.go b/vendor/github.com/go-openapi/loads/spec.go index 6d967389b..649ca06e6 100644 --- a/vendor/github.com/go-openapi/loads/spec.go +++ b/vendor/github.com/go-openapi/loads/spec.go @@ -20,8 +20,6 @@ import ( "fmt" "net/url" - "path/filepath" - "github.com/go-openapi/analysis" "github.com/go-openapi/spec" "github.com/go-openapi/swag" @@ -92,6 +90,22 @@ type Document struct { raw json.RawMessage } +// Embedded returns a Document based on embedded specs. No analysis is required +func Embedded(orig, flat json.RawMessage) (*Document, error) { + var origSpec, flatSpec spec.Swagger + if err := json.Unmarshal(orig, &origSpec); err != nil { + return nil, err + } + if err := json.Unmarshal(flat, &flatSpec); err != nil { + return nil, err + } + return &Document{ + raw: orig, + origSpec: &origSpec, + spec: &flatSpec, + }, nil +} + // Spec loads a new spec document func Spec(path string) (*Document, error) { specURL, err := url.Parse(path) @@ -186,10 +200,10 @@ func (d *Document) Expanded(options ...*spec.ExpandOptions) (*Document, error) { var expandOptions *spec.ExpandOptions if len(options) > 0 { - expandOptions = options[1] + expandOptions = options[0] } else { expandOptions = &spec.ExpandOptions{ - RelativeBase: filepath.Dir(d.specFilePath), + RelativeBase: d.specFilePath, } } @@ -198,11 +212,12 @@ func (d *Document) Expanded(options ...*spec.ExpandOptions) (*Document, error) { } dd := &Document{ - Analyzer: analysis.New(swspec), - spec: swspec, - schema: spec.MustLoadSwagger20Schema(), - raw: d.raw, - origSpec: d.origSpec, + Analyzer: analysis.New(swspec), + spec: swspec, + specFilePath: d.specFilePath, + schema: spec.MustLoadSwagger20Schema(), + raw: d.raw, + origSpec: d.origSpec, } return dd, nil } diff --git a/vendor/github.com/golang/protobuf/jsonpb/jsonpb.go b/vendor/github.com/golang/protobuf/jsonpb/jsonpb.go new file mode 100644 index 000000000..110ae1384 --- /dev/null +++ b/vendor/github.com/golang/protobuf/jsonpb/jsonpb.go @@ -0,0 +1,1083 @@ +// Go support for Protocol Buffers - Google's data interchange format +// +// Copyright 2015 The Go Authors. All rights reserved. +// https://github.com/golang/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* +Package jsonpb provides marshaling and unmarshaling between protocol buffers and JSON. +It follows the specification at https://developers.google.com/protocol-buffers/docs/proto3#json. + +This package produces a different output than the standard "encoding/json" package, +which does not operate correctly on protocol buffers. +*/ +package jsonpb + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/golang/protobuf/proto" + + stpb "github.com/golang/protobuf/ptypes/struct" +) + +// Marshaler is a configurable object for converting between +// protocol buffer objects and a JSON representation for them. +type Marshaler struct { + // Whether to render enum values as integers, as opposed to string values. + EnumsAsInts bool + + // Whether to render fields with zero values. + EmitDefaults bool + + // A string to indent each level by. The presence of this field will + // also cause a space to appear between the field separator and + // value, and for newlines to be appear between fields and array + // elements. + Indent string + + // Whether to use the original (.proto) name for fields. + OrigName bool + + // A custom URL resolver to use when marshaling Any messages to JSON. + // If unset, the default resolution strategy is to extract the + // fully-qualified type name from the type URL and pass that to + // proto.MessageType(string). + AnyResolver AnyResolver +} + +// AnyResolver takes a type URL, present in an Any message, and resolves it into +// an instance of the associated message. +type AnyResolver interface { + Resolve(typeUrl string) (proto.Message, error) +} + +func defaultResolveAny(typeUrl string) (proto.Message, error) { + // Only the part of typeUrl after the last slash is relevant. + mname := typeUrl + if slash := strings.LastIndex(mname, "/"); slash >= 0 { + mname = mname[slash+1:] + } + mt := proto.MessageType(mname) + if mt == nil { + return nil, fmt.Errorf("unknown message type %q", mname) + } + return reflect.New(mt.Elem()).Interface().(proto.Message), nil +} + +// JSONPBMarshaler is implemented by protobuf messages that customize the +// way they are marshaled to JSON. Messages that implement this should +// also implement JSONPBUnmarshaler so that the custom format can be +// parsed. +type JSONPBMarshaler interface { + MarshalJSONPB(*Marshaler) ([]byte, error) +} + +// JSONPBUnmarshaler is implemented by protobuf messages that customize +// the way they are unmarshaled from JSON. Messages that implement this +// should also implement JSONPBMarshaler so that the custom format can be +// produced. +type JSONPBUnmarshaler interface { + UnmarshalJSONPB(*Unmarshaler, []byte) error +} + +// Marshal marshals a protocol buffer into JSON. +func (m *Marshaler) Marshal(out io.Writer, pb proto.Message) error { + writer := &errWriter{writer: out} + return m.marshalObject(writer, pb, "", "") +} + +// MarshalToString converts a protocol buffer object to JSON string. +func (m *Marshaler) MarshalToString(pb proto.Message) (string, error) { + var buf bytes.Buffer + if err := m.Marshal(&buf, pb); err != nil { + return "", err + } + return buf.String(), nil +} + +type int32Slice []int32 + +var nonFinite = map[string]float64{ + `"NaN"`: math.NaN(), + `"Infinity"`: math.Inf(1), + `"-Infinity"`: math.Inf(-1), +} + +// For sorting extensions ids to ensure stable output. +func (s int32Slice) Len() int { return len(s) } +func (s int32Slice) Less(i, j int) bool { return s[i] < s[j] } +func (s int32Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +type wkt interface { + XXX_WellKnownType() string +} + +// marshalObject writes a struct to the Writer. +func (m *Marshaler) marshalObject(out *errWriter, v proto.Message, indent, typeURL string) error { + if jsm, ok := v.(JSONPBMarshaler); ok { + b, err := jsm.MarshalJSONPB(m) + if err != nil { + return err + } + if typeURL != "" { + // we are marshaling this object to an Any type + var js map[string]*json.RawMessage + if err = json.Unmarshal(b, &js); err != nil { + return fmt.Errorf("type %T produced invalid JSON: %v", v, err) + } + turl, err := json.Marshal(typeURL) + if err != nil { + return fmt.Errorf("failed to marshal type URL %q to JSON: %v", typeURL, err) + } + js["@type"] = (*json.RawMessage)(&turl) + if b, err = json.Marshal(js); err != nil { + return err + } + } + + out.write(string(b)) + return out.err + } + + s := reflect.ValueOf(v).Elem() + + // Handle well-known types. + if wkt, ok := v.(wkt); ok { + switch wkt.XXX_WellKnownType() { + case "DoubleValue", "FloatValue", "Int64Value", "UInt64Value", + "Int32Value", "UInt32Value", "BoolValue", "StringValue", "BytesValue": + // "Wrappers use the same representation in JSON + // as the wrapped primitive type, ..." + sprop := proto.GetProperties(s.Type()) + return m.marshalValue(out, sprop.Prop[0], s.Field(0), indent) + case "Any": + // Any is a bit more involved. + return m.marshalAny(out, v, indent) + case "Duration": + // "Generated output always contains 3, 6, or 9 fractional digits, + // depending on required precision." + s, ns := s.Field(0).Int(), s.Field(1).Int() + d := time.Duration(s)*time.Second + time.Duration(ns)*time.Nanosecond + x := fmt.Sprintf("%.9f", d.Seconds()) + x = strings.TrimSuffix(x, "000") + x = strings.TrimSuffix(x, "000") + out.write(`"`) + out.write(x) + out.write(`s"`) + return out.err + case "Struct", "ListValue": + // Let marshalValue handle the `Struct.fields` map or the `ListValue.values` slice. + // TODO: pass the correct Properties if needed. + return m.marshalValue(out, &proto.Properties{}, s.Field(0), indent) + case "Timestamp": + // "RFC 3339, where generated output will always be Z-normalized + // and uses 3, 6 or 9 fractional digits." + s, ns := s.Field(0).Int(), s.Field(1).Int() + t := time.Unix(s, ns).UTC() + // time.RFC3339Nano isn't exactly right (we need to get 3/6/9 fractional digits). + x := t.Format("2006-01-02T15:04:05.000000000") + x = strings.TrimSuffix(x, "000") + x = strings.TrimSuffix(x, "000") + out.write(`"`) + out.write(x) + out.write(`Z"`) + return out.err + case "Value": + // Value has a single oneof. + kind := s.Field(0) + if kind.IsNil() { + // "absence of any variant indicates an error" + return errors.New("nil Value") + } + // oneof -> *T -> T -> T.F + x := kind.Elem().Elem().Field(0) + // TODO: pass the correct Properties if needed. + return m.marshalValue(out, &proto.Properties{}, x, indent) + } + } + + out.write("{") + if m.Indent != "" { + out.write("\n") + } + + firstField := true + + if typeURL != "" { + if err := m.marshalTypeURL(out, indent, typeURL); err != nil { + return err + } + firstField = false + } + + for i := 0; i < s.NumField(); i++ { + value := s.Field(i) + valueField := s.Type().Field(i) + if strings.HasPrefix(valueField.Name, "XXX_") { + continue + } + + // IsNil will panic on most value kinds. + switch value.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface: + if value.IsNil() { + continue + } + } + + if !m.EmitDefaults { + switch value.Kind() { + case reflect.Bool: + if !value.Bool() { + continue + } + case reflect.Int32, reflect.Int64: + if value.Int() == 0 { + continue + } + case reflect.Uint32, reflect.Uint64: + if value.Uint() == 0 { + continue + } + case reflect.Float32, reflect.Float64: + if value.Float() == 0 { + continue + } + case reflect.String: + if value.Len() == 0 { + continue + } + case reflect.Map, reflect.Ptr, reflect.Slice: + if value.IsNil() { + continue + } + } + } + + // Oneof fields need special handling. + if valueField.Tag.Get("protobuf_oneof") != "" { + // value is an interface containing &T{real_value}. + sv := value.Elem().Elem() // interface -> *T -> T + value = sv.Field(0) + valueField = sv.Type().Field(0) + } + prop := jsonProperties(valueField, m.OrigName) + if !firstField { + m.writeSep(out) + } + if err := m.marshalField(out, prop, value, indent); err != nil { + return err + } + firstField = false + } + + // Handle proto2 extensions. + if ep, ok := v.(proto.Message); ok { + extensions := proto.RegisteredExtensions(v) + // Sort extensions for stable output. + ids := make([]int32, 0, len(extensions)) + for id, desc := range extensions { + if !proto.HasExtension(ep, desc) { + continue + } + ids = append(ids, id) + } + sort.Sort(int32Slice(ids)) + for _, id := range ids { + desc := extensions[id] + if desc == nil { + // unknown extension + continue + } + ext, extErr := proto.GetExtension(ep, desc) + if extErr != nil { + return extErr + } + value := reflect.ValueOf(ext) + var prop proto.Properties + prop.Parse(desc.Tag) + prop.JSONName = fmt.Sprintf("[%s]", desc.Name) + if !firstField { + m.writeSep(out) + } + if err := m.marshalField(out, &prop, value, indent); err != nil { + return err + } + firstField = false + } + + } + + if m.Indent != "" { + out.write("\n") + out.write(indent) + } + out.write("}") + return out.err +} + +func (m *Marshaler) writeSep(out *errWriter) { + if m.Indent != "" { + out.write(",\n") + } else { + out.write(",") + } +} + +func (m *Marshaler) marshalAny(out *errWriter, any proto.Message, indent string) error { + // "If the Any contains a value that has a special JSON mapping, + // it will be converted as follows: {"@type": xxx, "value": yyy}. + // Otherwise, the value will be converted into a JSON object, + // and the "@type" field will be inserted to indicate the actual data type." + v := reflect.ValueOf(any).Elem() + turl := v.Field(0).String() + val := v.Field(1).Bytes() + + var msg proto.Message + var err error + if m.AnyResolver != nil { + msg, err = m.AnyResolver.Resolve(turl) + } else { + msg, err = defaultResolveAny(turl) + } + if err != nil { + return err + } + + if err := proto.Unmarshal(val, msg); err != nil { + return err + } + + if _, ok := msg.(wkt); ok { + out.write("{") + if m.Indent != "" { + out.write("\n") + } + if err := m.marshalTypeURL(out, indent, turl); err != nil { + return err + } + m.writeSep(out) + if m.Indent != "" { + out.write(indent) + out.write(m.Indent) + out.write(`"value": `) + } else { + out.write(`"value":`) + } + if err := m.marshalObject(out, msg, indent+m.Indent, ""); err != nil { + return err + } + if m.Indent != "" { + out.write("\n") + out.write(indent) + } + out.write("}") + return out.err + } + + return m.marshalObject(out, msg, indent, turl) +} + +func (m *Marshaler) marshalTypeURL(out *errWriter, indent, typeURL string) error { + if m.Indent != "" { + out.write(indent) + out.write(m.Indent) + } + out.write(`"@type":`) + if m.Indent != "" { + out.write(" ") + } + b, err := json.Marshal(typeURL) + if err != nil { + return err + } + out.write(string(b)) + return out.err +} + +// marshalField writes field description and value to the Writer. +func (m *Marshaler) marshalField(out *errWriter, prop *proto.Properties, v reflect.Value, indent string) error { + if m.Indent != "" { + out.write(indent) + out.write(m.Indent) + } + out.write(`"`) + out.write(prop.JSONName) + out.write(`":`) + if m.Indent != "" { + out.write(" ") + } + if err := m.marshalValue(out, prop, v, indent); err != nil { + return err + } + return nil +} + +// marshalValue writes the value to the Writer. +func (m *Marshaler) marshalValue(out *errWriter, prop *proto.Properties, v reflect.Value, indent string) error { + var err error + v = reflect.Indirect(v) + + // Handle nil pointer + if v.Kind() == reflect.Invalid { + out.write("null") + return out.err + } + + // Handle repeated elements. + if v.Kind() == reflect.Slice && v.Type().Elem().Kind() != reflect.Uint8 { + out.write("[") + comma := "" + for i := 0; i < v.Len(); i++ { + sliceVal := v.Index(i) + out.write(comma) + if m.Indent != "" { + out.write("\n") + out.write(indent) + out.write(m.Indent) + out.write(m.Indent) + } + if err := m.marshalValue(out, prop, sliceVal, indent+m.Indent); err != nil { + return err + } + comma = "," + } + if m.Indent != "" { + out.write("\n") + out.write(indent) + out.write(m.Indent) + } + out.write("]") + return out.err + } + + // Handle well-known types. + // Most are handled up in marshalObject (because 99% are messages). + if wkt, ok := v.Interface().(wkt); ok { + switch wkt.XXX_WellKnownType() { + case "NullValue": + out.write("null") + return out.err + } + } + + // Handle enumerations. + if !m.EnumsAsInts && prop.Enum != "" { + // Unknown enum values will are stringified by the proto library as their + // value. Such values should _not_ be quoted or they will be interpreted + // as an enum string instead of their value. + enumStr := v.Interface().(fmt.Stringer).String() + var valStr string + if v.Kind() == reflect.Ptr { + valStr = strconv.Itoa(int(v.Elem().Int())) + } else { + valStr = strconv.Itoa(int(v.Int())) + } + isKnownEnum := enumStr != valStr + if isKnownEnum { + out.write(`"`) + } + out.write(enumStr) + if isKnownEnum { + out.write(`"`) + } + return out.err + } + + // Handle nested messages. + if v.Kind() == reflect.Struct { + return m.marshalObject(out, v.Addr().Interface().(proto.Message), indent+m.Indent, "") + } + + // Handle maps. + // Since Go randomizes map iteration, we sort keys for stable output. + if v.Kind() == reflect.Map { + out.write(`{`) + keys := v.MapKeys() + sort.Sort(mapKeys(keys)) + for i, k := range keys { + if i > 0 { + out.write(`,`) + } + if m.Indent != "" { + out.write("\n") + out.write(indent) + out.write(m.Indent) + out.write(m.Indent) + } + + b, err := json.Marshal(k.Interface()) + if err != nil { + return err + } + s := string(b) + + // If the JSON is not a string value, encode it again to make it one. + if !strings.HasPrefix(s, `"`) { + b, err := json.Marshal(s) + if err != nil { + return err + } + s = string(b) + } + + out.write(s) + out.write(`:`) + if m.Indent != "" { + out.write(` `) + } + + if err := m.marshalValue(out, prop, v.MapIndex(k), indent+m.Indent); err != nil { + return err + } + } + if m.Indent != "" { + out.write("\n") + out.write(indent) + out.write(m.Indent) + } + out.write(`}`) + return out.err + } + + // Handle non-finite floats, e.g. NaN, Infinity and -Infinity. + if v.Kind() == reflect.Float32 || v.Kind() == reflect.Float64 { + f := v.Float() + var sval string + switch { + case math.IsInf(f, 1): + sval = `"Infinity"` + case math.IsInf(f, -1): + sval = `"-Infinity"` + case math.IsNaN(f): + sval = `"NaN"` + } + if sval != "" { + out.write(sval) + return out.err + } + } + + // Default handling defers to the encoding/json library. + b, err := json.Marshal(v.Interface()) + if err != nil { + return err + } + needToQuote := string(b[0]) != `"` && (v.Kind() == reflect.Int64 || v.Kind() == reflect.Uint64) + if needToQuote { + out.write(`"`) + } + out.write(string(b)) + if needToQuote { + out.write(`"`) + } + return out.err +} + +// Unmarshaler is a configurable object for converting from a JSON +// representation to a protocol buffer object. +type Unmarshaler struct { + // Whether to allow messages to contain unknown fields, as opposed to + // failing to unmarshal. + AllowUnknownFields bool + + // A custom URL resolver to use when unmarshaling Any messages from JSON. + // If unset, the default resolution strategy is to extract the + // fully-qualified type name from the type URL and pass that to + // proto.MessageType(string). + AnyResolver AnyResolver +} + +// UnmarshalNext unmarshals the next protocol buffer from a JSON object stream. +// This function is lenient and will decode any options permutations of the +// related Marshaler. +func (u *Unmarshaler) UnmarshalNext(dec *json.Decoder, pb proto.Message) error { + inputValue := json.RawMessage{} + if err := dec.Decode(&inputValue); err != nil { + return err + } + return u.unmarshalValue(reflect.ValueOf(pb).Elem(), inputValue, nil) +} + +// Unmarshal unmarshals a JSON object stream into a protocol +// buffer. This function is lenient and will decode any options +// permutations of the related Marshaler. +func (u *Unmarshaler) Unmarshal(r io.Reader, pb proto.Message) error { + dec := json.NewDecoder(r) + return u.UnmarshalNext(dec, pb) +} + +// UnmarshalNext unmarshals the next protocol buffer from a JSON object stream. +// This function is lenient and will decode any options permutations of the +// related Marshaler. +func UnmarshalNext(dec *json.Decoder, pb proto.Message) error { + return new(Unmarshaler).UnmarshalNext(dec, pb) +} + +// Unmarshal unmarshals a JSON object stream into a protocol +// buffer. This function is lenient and will decode any options +// permutations of the related Marshaler. +func Unmarshal(r io.Reader, pb proto.Message) error { + return new(Unmarshaler).Unmarshal(r, pb) +} + +// UnmarshalString will populate the fields of a protocol buffer based +// on a JSON string. This function is lenient and will decode any options +// permutations of the related Marshaler. +func UnmarshalString(str string, pb proto.Message) error { + return new(Unmarshaler).Unmarshal(strings.NewReader(str), pb) +} + +// unmarshalValue converts/copies a value into the target. +// prop may be nil. +func (u *Unmarshaler) unmarshalValue(target reflect.Value, inputValue json.RawMessage, prop *proto.Properties) error { + targetType := target.Type() + + // Allocate memory for pointer fields. + if targetType.Kind() == reflect.Ptr { + // If input value is "null" and target is a pointer type, then the field should be treated as not set + // UNLESS the target is structpb.Value, in which case it should be set to structpb.NullValue. + _, isJSONPBUnmarshaler := target.Interface().(JSONPBUnmarshaler) + if string(inputValue) == "null" && targetType != reflect.TypeOf(&stpb.Value{}) && !isJSONPBUnmarshaler { + return nil + } + target.Set(reflect.New(targetType.Elem())) + + return u.unmarshalValue(target.Elem(), inputValue, prop) + } + + if jsu, ok := target.Addr().Interface().(JSONPBUnmarshaler); ok { + return jsu.UnmarshalJSONPB(u, []byte(inputValue)) + } + + // Handle well-known types that are not pointers. + if w, ok := target.Addr().Interface().(wkt); ok { + switch w.XXX_WellKnownType() { + case "DoubleValue", "FloatValue", "Int64Value", "UInt64Value", + "Int32Value", "UInt32Value", "BoolValue", "StringValue", "BytesValue": + return u.unmarshalValue(target.Field(0), inputValue, prop) + case "Any": + // Use json.RawMessage pointer type instead of value to support pre-1.8 version. + // 1.8 changed RawMessage.MarshalJSON from pointer type to value type, see + // https://github.com/golang/go/issues/14493 + var jsonFields map[string]*json.RawMessage + if err := json.Unmarshal(inputValue, &jsonFields); err != nil { + return err + } + + val, ok := jsonFields["@type"] + if !ok || val == nil { + return errors.New("Any JSON doesn't have '@type'") + } + + var turl string + if err := json.Unmarshal([]byte(*val), &turl); err != nil { + return fmt.Errorf("can't unmarshal Any's '@type': %q", *val) + } + target.Field(0).SetString(turl) + + var m proto.Message + var err error + if u.AnyResolver != nil { + m, err = u.AnyResolver.Resolve(turl) + } else { + m, err = defaultResolveAny(turl) + } + if err != nil { + return err + } + + if _, ok := m.(wkt); ok { + val, ok := jsonFields["value"] + if !ok { + return errors.New("Any JSON doesn't have 'value'") + } + + if err := u.unmarshalValue(reflect.ValueOf(m).Elem(), *val, nil); err != nil { + return fmt.Errorf("can't unmarshal Any nested proto %T: %v", m, err) + } + } else { + delete(jsonFields, "@type") + nestedProto, err := json.Marshal(jsonFields) + if err != nil { + return fmt.Errorf("can't generate JSON for Any's nested proto to be unmarshaled: %v", err) + } + + if err = u.unmarshalValue(reflect.ValueOf(m).Elem(), nestedProto, nil); err != nil { + return fmt.Errorf("can't unmarshal Any nested proto %T: %v", m, err) + } + } + + b, err := proto.Marshal(m) + if err != nil { + return fmt.Errorf("can't marshal proto %T into Any.Value: %v", m, err) + } + target.Field(1).SetBytes(b) + + return nil + case "Duration": + unq, err := strconv.Unquote(string(inputValue)) + if err != nil { + return err + } + + d, err := time.ParseDuration(unq) + if err != nil { + return fmt.Errorf("bad Duration: %v", err) + } + + ns := d.Nanoseconds() + s := ns / 1e9 + ns %= 1e9 + target.Field(0).SetInt(s) + target.Field(1).SetInt(ns) + return nil + case "Timestamp": + unq, err := strconv.Unquote(string(inputValue)) + if err != nil { + return err + } + + t, err := time.Parse(time.RFC3339Nano, unq) + if err != nil { + return fmt.Errorf("bad Timestamp: %v", err) + } + + target.Field(0).SetInt(t.Unix()) + target.Field(1).SetInt(int64(t.Nanosecond())) + return nil + case "Struct": + var m map[string]json.RawMessage + if err := json.Unmarshal(inputValue, &m); err != nil { + return fmt.Errorf("bad StructValue: %v", err) + } + + target.Field(0).Set(reflect.ValueOf(map[string]*stpb.Value{})) + for k, jv := range m { + pv := &stpb.Value{} + if err := u.unmarshalValue(reflect.ValueOf(pv).Elem(), jv, prop); err != nil { + return fmt.Errorf("bad value in StructValue for key %q: %v", k, err) + } + target.Field(0).SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(pv)) + } + return nil + case "ListValue": + var s []json.RawMessage + if err := json.Unmarshal(inputValue, &s); err != nil { + return fmt.Errorf("bad ListValue: %v", err) + } + + target.Field(0).Set(reflect.ValueOf(make([]*stpb.Value, len(s), len(s)))) + for i, sv := range s { + if err := u.unmarshalValue(target.Field(0).Index(i), sv, prop); err != nil { + return err + } + } + return nil + case "Value": + ivStr := string(inputValue) + if ivStr == "null" { + target.Field(0).Set(reflect.ValueOf(&stpb.Value_NullValue{})) + } else if v, err := strconv.ParseFloat(ivStr, 0); err == nil { + target.Field(0).Set(reflect.ValueOf(&stpb.Value_NumberValue{v})) + } else if v, err := strconv.Unquote(ivStr); err == nil { + target.Field(0).Set(reflect.ValueOf(&stpb.Value_StringValue{v})) + } else if v, err := strconv.ParseBool(ivStr); err == nil { + target.Field(0).Set(reflect.ValueOf(&stpb.Value_BoolValue{v})) + } else if err := json.Unmarshal(inputValue, &[]json.RawMessage{}); err == nil { + lv := &stpb.ListValue{} + target.Field(0).Set(reflect.ValueOf(&stpb.Value_ListValue{lv})) + return u.unmarshalValue(reflect.ValueOf(lv).Elem(), inputValue, prop) + } else if err := json.Unmarshal(inputValue, &map[string]json.RawMessage{}); err == nil { + sv := &stpb.Struct{} + target.Field(0).Set(reflect.ValueOf(&stpb.Value_StructValue{sv})) + return u.unmarshalValue(reflect.ValueOf(sv).Elem(), inputValue, prop) + } else { + return fmt.Errorf("unrecognized type for Value %q", ivStr) + } + return nil + } + } + + // Handle enums, which have an underlying type of int32, + // and may appear as strings. + // The case of an enum appearing as a number is handled + // at the bottom of this function. + if inputValue[0] == '"' && prop != nil && prop.Enum != "" { + vmap := proto.EnumValueMap(prop.Enum) + // Don't need to do unquoting; valid enum names + // are from a limited character set. + s := inputValue[1 : len(inputValue)-1] + n, ok := vmap[string(s)] + if !ok { + return fmt.Errorf("unknown value %q for enum %s", s, prop.Enum) + } + if target.Kind() == reflect.Ptr { // proto2 + target.Set(reflect.New(targetType.Elem())) + target = target.Elem() + } + target.SetInt(int64(n)) + return nil + } + + // Handle nested messages. + if targetType.Kind() == reflect.Struct { + var jsonFields map[string]json.RawMessage + if err := json.Unmarshal(inputValue, &jsonFields); err != nil { + return err + } + + consumeField := func(prop *proto.Properties) (json.RawMessage, bool) { + // Be liberal in what names we accept; both orig_name and camelName are okay. + fieldNames := acceptedJSONFieldNames(prop) + + vOrig, okOrig := jsonFields[fieldNames.orig] + vCamel, okCamel := jsonFields[fieldNames.camel] + if !okOrig && !okCamel { + return nil, false + } + // If, for some reason, both are present in the data, favour the camelName. + var raw json.RawMessage + if okOrig { + raw = vOrig + delete(jsonFields, fieldNames.orig) + } + if okCamel { + raw = vCamel + delete(jsonFields, fieldNames.camel) + } + return raw, true + } + + sprops := proto.GetProperties(targetType) + for i := 0; i < target.NumField(); i++ { + ft := target.Type().Field(i) + if strings.HasPrefix(ft.Name, "XXX_") { + continue + } + + valueForField, ok := consumeField(sprops.Prop[i]) + if !ok { + continue + } + + if err := u.unmarshalValue(target.Field(i), valueForField, sprops.Prop[i]); err != nil { + return err + } + } + // Check for any oneof fields. + if len(jsonFields) > 0 { + for _, oop := range sprops.OneofTypes { + raw, ok := consumeField(oop.Prop) + if !ok { + continue + } + nv := reflect.New(oop.Type.Elem()) + target.Field(oop.Field).Set(nv) + if err := u.unmarshalValue(nv.Elem().Field(0), raw, oop.Prop); err != nil { + return err + } + } + } + // Handle proto2 extensions. + if len(jsonFields) > 0 { + if ep, ok := target.Addr().Interface().(proto.Message); ok { + for _, ext := range proto.RegisteredExtensions(ep) { + name := fmt.Sprintf("[%s]", ext.Name) + raw, ok := jsonFields[name] + if !ok { + continue + } + delete(jsonFields, name) + nv := reflect.New(reflect.TypeOf(ext.ExtensionType).Elem()) + if err := u.unmarshalValue(nv.Elem(), raw, nil); err != nil { + return err + } + if err := proto.SetExtension(ep, ext, nv.Interface()); err != nil { + return err + } + } + } + } + if !u.AllowUnknownFields && len(jsonFields) > 0 { + // Pick any field to be the scapegoat. + var f string + for fname := range jsonFields { + f = fname + break + } + return fmt.Errorf("unknown field %q in %v", f, targetType) + } + return nil + } + + // Handle arrays (which aren't encoded bytes) + if targetType.Kind() == reflect.Slice && targetType.Elem().Kind() != reflect.Uint8 { + var slc []json.RawMessage + if err := json.Unmarshal(inputValue, &slc); err != nil { + return err + } + if slc != nil { + l := len(slc) + target.Set(reflect.MakeSlice(targetType, l, l)) + for i := 0; i < l; i++ { + if err := u.unmarshalValue(target.Index(i), slc[i], prop); err != nil { + return err + } + } + } + return nil + } + + // Handle maps (whose keys are always strings) + if targetType.Kind() == reflect.Map { + var mp map[string]json.RawMessage + if err := json.Unmarshal(inputValue, &mp); err != nil { + return err + } + if mp != nil { + target.Set(reflect.MakeMap(targetType)) + var keyprop, valprop *proto.Properties + if prop != nil { + // These could still be nil if the protobuf metadata is broken somehow. + // TODO: This won't work because the fields are unexported. + // We should probably just reparse them. + //keyprop, valprop = prop.mkeyprop, prop.mvalprop + } + for ks, raw := range mp { + // Unmarshal map key. The core json library already decoded the key into a + // string, so we handle that specially. Other types were quoted post-serialization. + var k reflect.Value + if targetType.Key().Kind() == reflect.String { + k = reflect.ValueOf(ks) + } else { + k = reflect.New(targetType.Key()).Elem() + if err := u.unmarshalValue(k, json.RawMessage(ks), keyprop); err != nil { + return err + } + } + + // Unmarshal map value. + v := reflect.New(targetType.Elem()).Elem() + if err := u.unmarshalValue(v, raw, valprop); err != nil { + return err + } + target.SetMapIndex(k, v) + } + } + return nil + } + + // 64-bit integers can be encoded as strings. In this case we drop + // the quotes and proceed as normal. + isNum := targetType.Kind() == reflect.Int64 || targetType.Kind() == reflect.Uint64 + if isNum && strings.HasPrefix(string(inputValue), `"`) { + inputValue = inputValue[1 : len(inputValue)-1] + } + + // Non-finite numbers can be encoded as strings. + isFloat := targetType.Kind() == reflect.Float32 || targetType.Kind() == reflect.Float64 + if isFloat { + if num, ok := nonFinite[string(inputValue)]; ok { + target.SetFloat(num) + return nil + } + } + + // Use the encoding/json for parsing other value types. + return json.Unmarshal(inputValue, target.Addr().Interface()) +} + +// jsonProperties returns parsed proto.Properties for the field and corrects JSONName attribute. +func jsonProperties(f reflect.StructField, origName bool) *proto.Properties { + var prop proto.Properties + prop.Init(f.Type, f.Name, f.Tag.Get("protobuf"), &f) + if origName || prop.JSONName == "" { + prop.JSONName = prop.OrigName + } + return &prop +} + +type fieldNames struct { + orig, camel string +} + +func acceptedJSONFieldNames(prop *proto.Properties) fieldNames { + opts := fieldNames{orig: prop.OrigName, camel: prop.OrigName} + if prop.JSONName != "" { + opts.camel = prop.JSONName + } + return opts +} + +// Writer wrapper inspired by https://blog.golang.org/errors-are-values +type errWriter struct { + writer io.Writer + err error +} + +func (w *errWriter) write(str string) { + if w.err != nil { + return + } + _, w.err = w.writer.Write([]byte(str)) +} + +// Map fields may have key types of non-float scalars, strings and enums. +// The easiest way to sort them in some deterministic order is to use fmt. +// If this turns out to be inefficient we can always consider other options, +// such as doing a Schwartzian transform. +// +// Numeric keys are sorted in numeric order per +// https://developers.google.com/protocol-buffers/docs/proto#maps. +type mapKeys []reflect.Value + +func (s mapKeys) Len() int { return len(s) } +func (s mapKeys) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s mapKeys) Less(i, j int) bool { + if k := s[i].Kind(); k == s[j].Kind() { + switch k { + case reflect.Int32, reflect.Int64: + return s[i].Int() < s[j].Int() + case reflect.Uint32, reflect.Uint64: + return s[i].Uint() < s[j].Uint() + } + } + return fmt.Sprint(s[i].Interface()) < fmt.Sprint(s[j].Interface()) +} diff --git a/vendor/github.com/golang/protobuf/ptypes/struct/struct.pb.go b/vendor/github.com/golang/protobuf/ptypes/struct/struct.pb.go new file mode 100644 index 000000000..4cfe60818 --- /dev/null +++ b/vendor/github.com/golang/protobuf/ptypes/struct/struct.pb.go @@ -0,0 +1,380 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: google/protobuf/struct.proto + +/* +Package structpb is a generated protocol buffer package. + +It is generated from these files: + google/protobuf/struct.proto + +It has these top-level messages: + Struct + Value + ListValue +*/ +package structpb + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// `NullValue` is a singleton enumeration to represent the null value for the +// `Value` type union. +// +// The JSON representation for `NullValue` is JSON `null`. +type NullValue int32 + +const ( + // Null value. + NullValue_NULL_VALUE NullValue = 0 +) + +var NullValue_name = map[int32]string{ + 0: "NULL_VALUE", +} +var NullValue_value = map[string]int32{ + "NULL_VALUE": 0, +} + +func (x NullValue) String() string { + return proto.EnumName(NullValue_name, int32(x)) +} +func (NullValue) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +func (NullValue) XXX_WellKnownType() string { return "NullValue" } + +// `Struct` represents a structured data value, consisting of fields +// which map to dynamically typed values. In some languages, `Struct` +// might be supported by a native representation. For example, in +// scripting languages like JS a struct is represented as an +// object. The details of that representation are described together +// with the proto support for the language. +// +// The JSON representation for `Struct` is JSON object. +type Struct struct { + // Unordered map of dynamically typed values. + Fields map[string]*Value `protobuf:"bytes,1,rep,name=fields" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +} + +func (m *Struct) Reset() { *m = Struct{} } +func (m *Struct) String() string { return proto.CompactTextString(m) } +func (*Struct) ProtoMessage() {} +func (*Struct) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +func (*Struct) XXX_WellKnownType() string { return "Struct" } + +func (m *Struct) GetFields() map[string]*Value { + if m != nil { + return m.Fields + } + return nil +} + +// `Value` represents a dynamically typed value which can be either +// null, a number, a string, a boolean, a recursive struct value, or a +// list of values. A producer of value is expected to set one of that +// variants, absence of any variant indicates an error. +// +// The JSON representation for `Value` is JSON value. +type Value struct { + // The kind of value. + // + // Types that are valid to be assigned to Kind: + // *Value_NullValue + // *Value_NumberValue + // *Value_StringValue + // *Value_BoolValue + // *Value_StructValue + // *Value_ListValue + Kind isValue_Kind `protobuf_oneof:"kind"` +} + +func (m *Value) Reset() { *m = Value{} } +func (m *Value) String() string { return proto.CompactTextString(m) } +func (*Value) ProtoMessage() {} +func (*Value) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } +func (*Value) XXX_WellKnownType() string { return "Value" } + +type isValue_Kind interface { + isValue_Kind() +} + +type Value_NullValue struct { + NullValue NullValue `protobuf:"varint,1,opt,name=null_value,json=nullValue,enum=google.protobuf.NullValue,oneof"` +} +type Value_NumberValue struct { + NumberValue float64 `protobuf:"fixed64,2,opt,name=number_value,json=numberValue,oneof"` +} +type Value_StringValue struct { + StringValue string `protobuf:"bytes,3,opt,name=string_value,json=stringValue,oneof"` +} +type Value_BoolValue struct { + BoolValue bool `protobuf:"varint,4,opt,name=bool_value,json=boolValue,oneof"` +} +type Value_StructValue struct { + StructValue *Struct `protobuf:"bytes,5,opt,name=struct_value,json=structValue,oneof"` +} +type Value_ListValue struct { + ListValue *ListValue `protobuf:"bytes,6,opt,name=list_value,json=listValue,oneof"` +} + +func (*Value_NullValue) isValue_Kind() {} +func (*Value_NumberValue) isValue_Kind() {} +func (*Value_StringValue) isValue_Kind() {} +func (*Value_BoolValue) isValue_Kind() {} +func (*Value_StructValue) isValue_Kind() {} +func (*Value_ListValue) isValue_Kind() {} + +func (m *Value) GetKind() isValue_Kind { + if m != nil { + return m.Kind + } + return nil +} + +func (m *Value) GetNullValue() NullValue { + if x, ok := m.GetKind().(*Value_NullValue); ok { + return x.NullValue + } + return NullValue_NULL_VALUE +} + +func (m *Value) GetNumberValue() float64 { + if x, ok := m.GetKind().(*Value_NumberValue); ok { + return x.NumberValue + } + return 0 +} + +func (m *Value) GetStringValue() string { + if x, ok := m.GetKind().(*Value_StringValue); ok { + return x.StringValue + } + return "" +} + +func (m *Value) GetBoolValue() bool { + if x, ok := m.GetKind().(*Value_BoolValue); ok { + return x.BoolValue + } + return false +} + +func (m *Value) GetStructValue() *Struct { + if x, ok := m.GetKind().(*Value_StructValue); ok { + return x.StructValue + } + return nil +} + +func (m *Value) GetListValue() *ListValue { + if x, ok := m.GetKind().(*Value_ListValue); ok { + return x.ListValue + } + return nil +} + +// XXX_OneofFuncs is for the internal use of the proto package. +func (*Value) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) { + return _Value_OneofMarshaler, _Value_OneofUnmarshaler, _Value_OneofSizer, []interface{}{ + (*Value_NullValue)(nil), + (*Value_NumberValue)(nil), + (*Value_StringValue)(nil), + (*Value_BoolValue)(nil), + (*Value_StructValue)(nil), + (*Value_ListValue)(nil), + } +} + +func _Value_OneofMarshaler(msg proto.Message, b *proto.Buffer) error { + m := msg.(*Value) + // kind + switch x := m.Kind.(type) { + case *Value_NullValue: + b.EncodeVarint(1<<3 | proto.WireVarint) + b.EncodeVarint(uint64(x.NullValue)) + case *Value_NumberValue: + b.EncodeVarint(2<<3 | proto.WireFixed64) + b.EncodeFixed64(math.Float64bits(x.NumberValue)) + case *Value_StringValue: + b.EncodeVarint(3<<3 | proto.WireBytes) + b.EncodeStringBytes(x.StringValue) + case *Value_BoolValue: + t := uint64(0) + if x.BoolValue { + t = 1 + } + b.EncodeVarint(4<<3 | proto.WireVarint) + b.EncodeVarint(t) + case *Value_StructValue: + b.EncodeVarint(5<<3 | proto.WireBytes) + if err := b.EncodeMessage(x.StructValue); err != nil { + return err + } + case *Value_ListValue: + b.EncodeVarint(6<<3 | proto.WireBytes) + if err := b.EncodeMessage(x.ListValue); err != nil { + return err + } + case nil: + default: + return fmt.Errorf("Value.Kind has unexpected type %T", x) + } + return nil +} + +func _Value_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) { + m := msg.(*Value) + switch tag { + case 1: // kind.null_value + if wire != proto.WireVarint { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeVarint() + m.Kind = &Value_NullValue{NullValue(x)} + return true, err + case 2: // kind.number_value + if wire != proto.WireFixed64 { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeFixed64() + m.Kind = &Value_NumberValue{math.Float64frombits(x)} + return true, err + case 3: // kind.string_value + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeStringBytes() + m.Kind = &Value_StringValue{x} + return true, err + case 4: // kind.bool_value + if wire != proto.WireVarint { + return true, proto.ErrInternalBadWireType + } + x, err := b.DecodeVarint() + m.Kind = &Value_BoolValue{x != 0} + return true, err + case 5: // kind.struct_value + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + msg := new(Struct) + err := b.DecodeMessage(msg) + m.Kind = &Value_StructValue{msg} + return true, err + case 6: // kind.list_value + if wire != proto.WireBytes { + return true, proto.ErrInternalBadWireType + } + msg := new(ListValue) + err := b.DecodeMessage(msg) + m.Kind = &Value_ListValue{msg} + return true, err + default: + return false, nil + } +} + +func _Value_OneofSizer(msg proto.Message) (n int) { + m := msg.(*Value) + // kind + switch x := m.Kind.(type) { + case *Value_NullValue: + n += proto.SizeVarint(1<<3 | proto.WireVarint) + n += proto.SizeVarint(uint64(x.NullValue)) + case *Value_NumberValue: + n += proto.SizeVarint(2<<3 | proto.WireFixed64) + n += 8 + case *Value_StringValue: + n += proto.SizeVarint(3<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(len(x.StringValue))) + n += len(x.StringValue) + case *Value_BoolValue: + n += proto.SizeVarint(4<<3 | proto.WireVarint) + n += 1 + case *Value_StructValue: + s := proto.Size(x.StructValue) + n += proto.SizeVarint(5<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(s)) + n += s + case *Value_ListValue: + s := proto.Size(x.ListValue) + n += proto.SizeVarint(6<<3 | proto.WireBytes) + n += proto.SizeVarint(uint64(s)) + n += s + case nil: + default: + panic(fmt.Sprintf("proto: unexpected type %T in oneof", x)) + } + return n +} + +// `ListValue` is a wrapper around a repeated field of values. +// +// The JSON representation for `ListValue` is JSON array. +type ListValue struct { + // Repeated field of dynamically typed values. + Values []*Value `protobuf:"bytes,1,rep,name=values" json:"values,omitempty"` +} + +func (m *ListValue) Reset() { *m = ListValue{} } +func (m *ListValue) String() string { return proto.CompactTextString(m) } +func (*ListValue) ProtoMessage() {} +func (*ListValue) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } +func (*ListValue) XXX_WellKnownType() string { return "ListValue" } + +func (m *ListValue) GetValues() []*Value { + if m != nil { + return m.Values + } + return nil +} + +func init() { + proto.RegisterType((*Struct)(nil), "google.protobuf.Struct") + proto.RegisterType((*Value)(nil), "google.protobuf.Value") + proto.RegisterType((*ListValue)(nil), "google.protobuf.ListValue") + proto.RegisterEnum("google.protobuf.NullValue", NullValue_name, NullValue_value) +} + +func init() { proto.RegisterFile("google/protobuf/struct.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 417 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x92, 0x41, 0x8b, 0xd3, 0x40, + 0x14, 0xc7, 0x3b, 0xc9, 0x36, 0x98, 0x17, 0x59, 0x97, 0x11, 0xb4, 0xac, 0xa2, 0xa1, 0x7b, 0x09, + 0x22, 0x29, 0xd6, 0x8b, 0x18, 0x2f, 0x06, 0xd6, 0x5d, 0x30, 0x2c, 0x31, 0xba, 0x15, 0xbc, 0x94, + 0x26, 0x4d, 0x63, 0xe8, 0x74, 0x26, 0x24, 0x33, 0x4a, 0x8f, 0x7e, 0x0b, 0xcf, 0x1e, 0x3d, 0xfa, + 0xe9, 0x3c, 0xca, 0xcc, 0x24, 0xa9, 0xb4, 0xf4, 0x94, 0xbc, 0xf7, 0x7e, 0xef, 0x3f, 0xef, 0xff, + 0x66, 0xe0, 0x71, 0xc1, 0x58, 0x41, 0xf2, 0x49, 0x55, 0x33, 0xce, 0x52, 0xb1, 0x9a, 0x34, 0xbc, + 0x16, 0x19, 0xf7, 0x55, 0x8c, 0xef, 0xe9, 0xaa, 0xdf, 0x55, 0xc7, 0x3f, 0x11, 0x58, 0x1f, 0x15, + 0x81, 0x03, 0xb0, 0x56, 0x65, 0x4e, 0x96, 0xcd, 0x08, 0xb9, 0xa6, 0xe7, 0x4c, 0x2f, 0xfc, 0x3d, + 0xd8, 0xd7, 0xa0, 0xff, 0x4e, 0x51, 0x97, 0x94, 0xd7, 0xdb, 0xa4, 0x6d, 0x39, 0xff, 0x00, 0xce, + 0x7f, 0x69, 0x7c, 0x06, 0xe6, 0x3a, 0xdf, 0x8e, 0x90, 0x8b, 0x3c, 0x3b, 0x91, 0xbf, 0xf8, 0x39, + 0x0c, 0xbf, 0x2d, 0x88, 0xc8, 0x47, 0x86, 0x8b, 0x3c, 0x67, 0xfa, 0xe0, 0x40, 0x7c, 0x26, 0xab, + 0x89, 0x86, 0x5e, 0x1b, 0xaf, 0xd0, 0xf8, 0x8f, 0x01, 0x43, 0x95, 0xc4, 0x01, 0x00, 0x15, 0x84, + 0xcc, 0xb5, 0x80, 0x14, 0x3d, 0x9d, 0x9e, 0x1f, 0x08, 0xdc, 0x08, 0x42, 0x14, 0x7f, 0x3d, 0x48, + 0x6c, 0xda, 0x05, 0xf8, 0x02, 0xee, 0x52, 0xb1, 0x49, 0xf3, 0x7a, 0xbe, 0x3b, 0x1f, 0x5d, 0x0f, + 0x12, 0x47, 0x67, 0x7b, 0xa8, 0xe1, 0x75, 0x49, 0x8b, 0x16, 0x32, 0xe5, 0xe0, 0x12, 0xd2, 0x59, + 0x0d, 0x3d, 0x05, 0x48, 0x19, 0xeb, 0xc6, 0x38, 0x71, 0x91, 0x77, 0x47, 0x1e, 0x25, 0x73, 0x1a, + 0x78, 0xa3, 0x54, 0x44, 0xc6, 0x5b, 0x64, 0xa8, 0xac, 0x3e, 0x3c, 0xb2, 0xc7, 0x56, 0x5e, 0x64, + 0xbc, 0x77, 0x49, 0xca, 0xa6, 0xeb, 0xb5, 0x54, 0xef, 0xa1, 0xcb, 0xa8, 0x6c, 0x78, 0xef, 0x92, + 0x74, 0x41, 0x68, 0xc1, 0xc9, 0xba, 0xa4, 0xcb, 0x71, 0x00, 0x76, 0x4f, 0x60, 0x1f, 0x2c, 0x25, + 0xd6, 0xdd, 0xe8, 0xb1, 0xa5, 0xb7, 0xd4, 0xb3, 0x47, 0x60, 0xf7, 0x4b, 0xc4, 0xa7, 0x00, 0x37, + 0xb7, 0x51, 0x34, 0x9f, 0xbd, 0x8d, 0x6e, 0x2f, 0xcf, 0x06, 0xe1, 0x0f, 0x04, 0xf7, 0x33, 0xb6, + 0xd9, 0x97, 0x08, 0x1d, 0xed, 0x26, 0x96, 0x71, 0x8c, 0xbe, 0xbc, 0x28, 0x4a, 0xfe, 0x55, 0xa4, + 0x7e, 0xc6, 0x36, 0x93, 0x82, 0x91, 0x05, 0x2d, 0x76, 0x4f, 0xb1, 0xe2, 0xdb, 0x2a, 0x6f, 0xda, + 0x17, 0x19, 0xe8, 0x4f, 0x95, 0xfe, 0x45, 0xe8, 0x97, 0x61, 0x5e, 0xc5, 0xe1, 0x6f, 0xe3, 0xc9, + 0x95, 0x16, 0x8f, 0xbb, 0xf9, 0x3e, 0xe7, 0x84, 0xbc, 0xa7, 0xec, 0x3b, 0xfd, 0x24, 0x3b, 0x53, + 0x4b, 0x49, 0xbd, 0xfc, 0x17, 0x00, 0x00, 0xff, 0xff, 0xe8, 0x1b, 0x59, 0xf8, 0xe5, 0x02, 0x00, + 0x00, +} diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 93db8ddc3..41f8ed53d 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -21,6 +21,8 @@ import ( // invalid. var ErrBadHandshake = errors.New("websocket: bad handshake") +var errInvalidCompression = errors.New("websocket: invalid compression negotiation") + // NewClient creates a new client connection using the given net connection. // The URL u specifies the host and request URI. Use requestHeader to specify // the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies @@ -30,50 +32,17 @@ var ErrBadHandshake = errors.New("websocket: bad handshake") // If the WebSocket handshake fails, ErrBadHandshake is returned along with a // non-nil *http.Response so that callers can handle redirects, authentication, // etc. +// +// Deprecated: Use Dialer instead. func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { - challengeKey, err := generateChallengeKey() - if err != nil { - return nil, nil, err - } - acceptKey := computeAcceptKey(challengeKey) - - c = newConn(netConn, false, readBufSize, writeBufSize) - p := c.writeBuf[:0] - p = append(p, "GET "...) - p = append(p, u.RequestURI()...) - p = append(p, " HTTP/1.1\r\nHost: "...) - p = append(p, u.Host...) - // "Upgrade" is capitalized for servers that do not use case insensitive - // comparisons on header tokens. - p = append(p, "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...) - p = append(p, challengeKey...) - p = append(p, "\r\n"...) - for k, vs := range requestHeader { - for _, v := range vs { - p = append(p, k...) - p = append(p, ": "...) - p = append(p, v...) - p = append(p, "\r\n"...) - } - } - p = append(p, "\r\n"...) - - if _, err := netConn.Write(p); err != nil { - return nil, nil, err - } - - resp, err := http.ReadResponse(c.br, &http.Request{Method: "GET", URL: u}) - if err != nil { - return nil, nil, err - } - if resp.StatusCode != 101 || - !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || - !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || - resp.Header.Get("Sec-Websocket-Accept") != acceptKey { - return nil, resp, ErrBadHandshake + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, } - c.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") - return c, resp, nil + return d.Dial(u.String(), requestHeader) } // A Dialer contains options for connecting to WebSocket server. @@ -82,6 +51,12 @@ type Dialer struct { // NetDial is nil, net.Dial is used. NetDial func(network, addr string) (net.Conn, error) + // Proxy specifies a function to return a proxy for a given + // Request. If the function returns a non-nil error, the + // request is aborted with the provided error. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + // TLSClientConfig specifies the TLS configuration to use with tls.Client. // If nil, the default configuration is used. TLSClientConfig *tls.Config @@ -89,72 +64,54 @@ type Dialer struct { // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration - // Input and output buffer sizes. If the buffer size is zero, then a - // default value of 4096 is used. + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // size is zero, then a useful default size is used. The I/O buffer sizes + // do not limit the size of the messages that can be sent or received. ReadBufferSize, WriteBufferSize int // Subprotocols specifies the client's requested subprotocols. Subprotocols []string -} -var errMalformedURL = errors.New("malformed ws or wss URL") + // EnableCompression specifies if the client should attempt to negotiate + // per message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool -// parseURL parses the URL. The url.Parse function is not used here because -// url.Parse mangles the path. -func parseURL(s string) (*url.URL, error) { - // From the RFC: - // - // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] - // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] - // - // We don't use the net/url parser here because the dialer interface does - // not provide a way for applications to work around percent deocding in - // the net/url parser. - - var u url.URL - switch { - case strings.HasPrefix(s, "ws://"): - u.Scheme = "ws" - s = s[len("ws://"):] - case strings.HasPrefix(s, "wss://"): - u.Scheme = "wss" - s = s[len("wss://"):] - default: - return nil, errMalformedURL - } - - u.Host = s - u.Opaque = "/" - if i := strings.Index(s, "/"); i >= 0 { - u.Host = s[:i] - u.Opaque = s[i:] - } - - if strings.Contains(u.Host, "@") { - // WebSocket URIs do not contain user information. - return nil, errMalformedURL - } - - return &u, nil + // Jar specifies the cookie jar. + // If Jar is nil, cookies are not sent in requests and ignored + // in responses. + Jar http.CookieJar } +var errMalformedURL = errors.New("malformed ws or wss URL") + func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { hostPort = u.Host hostNoPort = u.Host if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { hostNoPort = hostNoPort[:i] } else { - if u.Scheme == "wss" { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": hostPort += ":443" - } else { + default: hostPort += ":80" } } return hostPort, hostNoPort } -// DefaultDialer is a dialer with all fields set to the default zero values. -var DefaultDialer *Dialer +// DefaultDialer is a dialer with all fields set to the default values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, +} + +// nilDialer is dialer to use when receiver is nil. +var nilDialer Dialer = *DefaultDialer // Dial creates a new client connection. Use requestHeader to specify the // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). @@ -166,15 +123,85 @@ var DefaultDialer *Dialer // etcetera. The response body may not contain the entire response and does not // need to be closed by the application. func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { - u, err := parseURL(urlStr) + + if d == nil { + d = &nilDialer + } + + challengeKey, err := generateChallengeKey() if err != nil { return nil, nil, err } - hostPort, hostNoPort := hostPortNoPort(u) + u, err := url.Parse(urlStr) + if err != nil { + return nil, nil, err + } - if d == nil { - d = &Dialer{} + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: "GET", + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + + // Set the cookies present in the cookie jar of the dialer + if d.Jar != nil { + for _, cookie := range d.Jar.Cookies(u) { + req.AddCookie(cookie) + } + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + k == "Sec-Websocket-Extensions" || + (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): + return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + case k == "Sec-Websocket-Protocol": + req.Header["Sec-WebSocket-Protocol"] = vs + default: + req.Header[k] = vs + } + } + + if d.EnableCompression { + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} } var deadline time.Time @@ -182,12 +209,46 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re deadline = time.Now().Add(d.HandshakeTimeout) } + // Get network dial function. netDial := d.NetDial if netDial == nil { netDialer := &net.Dialer{Deadline: deadline} netDial = netDialer.Dial } + // If needed, wrap the dial function to set the connection deadline. + if !deadline.Equal(time.Time{}) { + forwardDial := netDial + netDial = func(network, addr string) (net.Conn, error) { + c, err := forwardDial(network, addr) + if err != nil { + return nil, err + } + err = c.SetDeadline(deadline) + if err != nil { + c.Close() + return nil, err + } + return c, nil + } + } + + // If needed, wrap the dial function to connect through a proxy. + if d.Proxy != nil { + proxyURL, err := d.Proxy(req) + if err != nil { + return nil, nil, err + } + if proxyURL != nil { + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) + if err != nil { + return nil, nil, err + } + netDial = dialer.Dial + } + } + + hostPort, hostNoPort := hostPortNoPort(u) netConn, err := netDial("tcp", hostPort) if err != nil { return nil, nil, err @@ -199,17 +260,9 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } }() - if err := netConn.SetDeadline(deadline); err != nil { - return nil, nil, err - } - - if u.Scheme == "wss" { - cfg := d.TLSClientConfig - if cfg == nil { - cfg = &tls.Config{ServerName: hostNoPort} - } else if cfg.ServerName == "" { - shallowCopy := *cfg - cfg = &shallowCopy + if u.Scheme == "https" { + cfg := cloneTLSConfig(d.TLSClientConfig) + if cfg.ServerName == "" { cfg.ServerName = hostNoPort } tlsConn := tls.Client(netConn, cfg) @@ -224,45 +277,53 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } } - if len(d.Subprotocols) > 0 { - h := http.Header{} - for k, v := range requestHeader { - h[k] = v - } - h.Set("Sec-Websocket-Protocol", strings.Join(d.Subprotocols, ", ")) - requestHeader = h + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + + if err := req.Write(netConn); err != nil { + return nil, nil, err } - if len(requestHeader["Host"]) > 0 { - // This can be used to supply a Host: header which is different from - // the dial address. - u.Host = requestHeader.Get("Host") + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } - // Drop "Host" header - h := http.Header{} - for k, v := range requestHeader { - if k == "Host" { - continue - } - h[k] = v + if d.Jar != nil { + if rc := resp.Cookies(); len(rc) > 0 { + d.Jar.SetCookies(u, rc) } - requestHeader = h } - conn, resp, err := NewClient(netConn, u, requestHeader, d.ReadBufferSize, d.WriteBufferSize) + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { + // Before closing the network connection on return from this + // function, slurp up some of the response to aid application + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } - if err != nil { - if err == ErrBadHandshake { - // Before closing the network connection on return from this - // function, slurp up some of the response to aid application - // debugging. - buf := make([]byte, 1024) - n, _ := io.ReadFull(resp.Body, buf) - resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + for _, ext := range parseExtensions(resp.Header) { + if ext[""] != "permessage-deflate" { + continue } - return nil, resp, err + _, snct := ext["server_no_context_takeover"] + _, cnct := ext["client_no_context_takeover"] + if !snct || !cnct { + return nil, resp, errInvalidCompression + } + conn.newCompressionWriter = compressNoContextTakeover + conn.newDecompressionReader = decompressNoContextTakeover + break } + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + netConn.SetDeadline(time.Time{}) netConn = nil // to avoid close in defer. return conn, resp, nil diff --git a/vendor/github.com/gorilla/websocket/client_clone.go b/vendor/github.com/gorilla/websocket/client_clone.go new file mode 100644 index 000000000..4f0d94372 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client_clone.go @@ -0,0 +1,16 @@ +// 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. + +// +build go1.8 + +package websocket + +import "crypto/tls" + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return cfg.Clone() +} diff --git a/vendor/github.com/gorilla/websocket/client_clone_legacy.go b/vendor/github.com/gorilla/websocket/client_clone_legacy.go new file mode 100644 index 000000000..babb007fb --- /dev/null +++ b/vendor/github.com/gorilla/websocket/client_clone_legacy.go @@ -0,0 +1,38 @@ +// 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. + +// +build !go1.8 + +package websocket + +import "crypto/tls" + +// cloneTLSConfig clones all public fields except the fields +// SessionTicketsDisabled and SessionTicketKey. This avoids copying the +// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a +// config in active use. +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return &tls.Config{ + Rand: cfg.Rand, + Time: cfg.Time, + Certificates: cfg.Certificates, + NameToCertificate: cfg.NameToCertificate, + GetCertificate: cfg.GetCertificate, + RootCAs: cfg.RootCAs, + NextProtos: cfg.NextProtos, + ServerName: cfg.ServerName, + ClientAuth: cfg.ClientAuth, + ClientCAs: cfg.ClientCAs, + InsecureSkipVerify: cfg.InsecureSkipVerify, + CipherSuites: cfg.CipherSuites, + PreferServerCipherSuites: cfg.PreferServerCipherSuites, + ClientSessionCache: cfg.ClientSessionCache, + MinVersion: cfg.MinVersion, + MaxVersion: cfg.MaxVersion, + CurvePreferences: cfg.CurvePreferences, + } +} diff --git a/vendor/github.com/gorilla/websocket/compression.go b/vendor/github.com/gorilla/websocket/compression.go new file mode 100644 index 000000000..813ffb1e8 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/compression.go @@ -0,0 +1,148 @@ +// Copyright 2017 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 ( + "compress/flate" + "errors" + "io" + "strings" + "sync" +) + +const ( + minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6 + maxCompressionLevel = flate.BestCompression + defaultCompressionLevel = 1 +) + +var ( + flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool + flateReaderPool = sync.Pool{New: func() interface{} { + return flate.NewReader(nil) + }} +) + +func decompressNoContextTakeover(r io.Reader) io.ReadCloser { + const tail = + // Add four bytes as specified in RFC + "\x00\x00\xff\xff" + + // Add final block to squelch unexpected EOF error from flate reader. + "\x01\x00\x00\xff\xff" + + fr, _ := flateReaderPool.Get().(io.ReadCloser) + fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil) + return &flateReadWrapper{fr} +} + +func isValidCompressionLevel(level int) bool { + return minCompressionLevel <= level && level <= maxCompressionLevel +} + +func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser { + p := &flateWriterPools[level-minCompressionLevel] + tw := &truncWriter{w: w} + fw, _ := p.Get().(*flate.Writer) + if fw == nil { + fw, _ = flate.NewWriter(tw, level) + } else { + fw.Reset(tw) + } + return &flateWriteWrapper{fw: fw, tw: tw, p: p} +} + +// truncWriter is an io.Writer that writes all but the last four bytes of the +// stream to another io.Writer. +type truncWriter struct { + w io.WriteCloser + n int + p [4]byte +} + +func (w *truncWriter) Write(p []byte) (int, error) { + n := 0 + + // fill buffer first for simplicity. + if w.n < len(w.p) { + n = copy(w.p[w.n:], p) + p = p[n:] + w.n += n + if len(p) == 0 { + return n, nil + } + } + + m := len(p) + if m > len(w.p) { + m = len(w.p) + } + + if nn, err := w.w.Write(w.p[:m]); err != nil { + return n + nn, err + } + + copy(w.p[:], w.p[m:]) + copy(w.p[len(w.p)-m:], p[len(p)-m:]) + nn, err := w.w.Write(p[:len(p)-m]) + return n + nn, err +} + +type flateWriteWrapper struct { + fw *flate.Writer + tw *truncWriter + p *sync.Pool +} + +func (w *flateWriteWrapper) Write(p []byte) (int, error) { + if w.fw == nil { + return 0, errWriteClosed + } + return w.fw.Write(p) +} + +func (w *flateWriteWrapper) Close() error { + if w.fw == nil { + return errWriteClosed + } + err1 := w.fw.Flush() + w.p.Put(w.fw) + w.fw = nil + if w.tw.p != [4]byte{0, 0, 0xff, 0xff} { + return errors.New("websocket: internal error, unexpected bytes at end of flate stream") + } + err2 := w.tw.w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +type flateReadWrapper struct { + fr io.ReadCloser +} + +func (r *flateReadWrapper) Read(p []byte) (int, error) { + if r.fr == nil { + return 0, io.ErrClosedPipe + } + n, err := r.fr.Read(p) + if err == io.EOF { + // Preemptively place the reader back in the pool. This helps with + // scenarios where the application does not call NextReader() soon after + // this final read. + r.Close() + } + return n, err +} + +func (r *flateReadWrapper) Close() error { + if r.fr == nil { + return io.ErrClosedPipe + } + err := r.fr.Close() + flateReaderPool.Put(r.fr) + r.fr = nil + return err +} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index e719f1ce6..5f46bf4a5 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -13,15 +13,25 @@ import ( "math/rand" "net" "strconv" + "sync" "time" + "unicode/utf8" ) const ( + // Frame header byte 0 bits from Section 5.2 of RFC 6455 + finalBit = 1 << 7 + rsv1Bit = 1 << 6 + rsv2Bit = 1 << 5 + rsv3Bit = 1 << 4 + + // Frame header byte 1 bits from Section 5.2 of RFC 6455 + maskBit = 1 << 7 + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask maxControlFramePayloadSize = 125 - finalBit = 1 << 7 - maskBit = 1 << 7 - writeWait = time.Second + + writeWait = time.Second defaultReadBufferSize = 4096 defaultWriteBufferSize = 4096 @@ -43,6 +53,8 @@ const ( CloseMessageTooBig = 1009 CloseMandatoryExtension = 1010 CloseInternalServerErr = 1011 + CloseServiceRestart = 1012 + CloseTryAgainLater = 1013 CloseTLSHandshake = 1015 ) @@ -64,7 +76,7 @@ const ( // is UTF-8 encoded text. PingMessage = 9 - // PongMessage denotes a ping control message. The optional message payload + // PongMessage denotes a pong control message. The optional message payload // is UTF-8 encoded text. PongMessage = 10 ) @@ -88,24 +100,91 @@ func (e *netError) Error() string { return e.msg } func (e *netError) Temporary() bool { return e.temporary } func (e *netError) Timeout() bool { return e.timeout } -// closeError represents close frame. -type closeError struct { - code int - text string +// CloseError represents a close message. +type CloseError struct { + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false } -func (e *closeError) Error() string { - return "websocket: close " + strconv.Itoa(e.code) + " " + e.text +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false } var ( - errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true} - errUnexpectedEOF = &closeError{code: CloseAbnormalClosure, text: io.ErrUnexpectedEOF.Error()} + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} errBadWriteOpCode = errors.New("websocket: bad write message type") errWriteClosed = errors.New("websocket: write closed") errInvalidControlFrame = errors.New("websocket: invalid control frame") ) +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + func hideTempErr(err error) error { if e, ok := err.(net.Error); ok && e.Temporary() { err = &netError{msg: e.Error(), timeout: e.Timeout()} @@ -121,72 +200,138 @@ func isData(frameType int) bool { return frameType == TextMessage || frameType == BinaryMessage } -func maskBytes(key [4]byte, pos int, b []byte) int { - for i := range b { - b[i] ^= key[pos&3] - pos++ - } - return pos & 3 +var validReceivedCloseCodes = map[int]bool{ + // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number + + CloseNormalClosure: true, + CloseGoingAway: true, + CloseProtocolError: true, + CloseUnsupportedData: true, + CloseNoStatusReceived: false, + CloseAbnormalClosure: false, + CloseInvalidFramePayloadData: true, + ClosePolicyViolation: true, + CloseMessageTooBig: true, + CloseMandatoryExtension: true, + CloseInternalServerErr: true, + CloseServiceRestart: true, + CloseTryAgainLater: true, + CloseTLSHandshake: false, } -func newMaskKey() [4]byte { - n := rand.Uint32() - return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +func isValidReceivedCloseCode(code int) bool { + return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) } -// Conn represents a WebSocket connection. +// The Conn type represents a WebSocket connection. type Conn struct { conn net.Conn isServer bool subprotocol string // Write fields - mu chan bool // used as mutex to protect write to conn and closeSent - closeSent bool // true if close message was sent + mu chan bool // used as mutex to protect write to conn + writeBuf []byte // frame is constructed in this buffer. + writeDeadline time.Time + writer io.WriteCloser // the current writer returned to the application + isWriting bool // for best-effort concurrent write detection - // Message writer fields. - writeErr error - writeBuf []byte // frame is constructed in this buffer. - writePos int // end of data in writeBuf. - writeFrameType int // type of the current frame. - writeSeq int // incremented to invalidate message writers. - writeDeadline time.Time + writeErrMu sync.Mutex + writeErr error + + enableWriteCompression bool + compressionLevel int + newCompressionWriter func(io.WriteCloser, int) io.WriteCloser // Read fields + reader io.ReadCloser // the current reader returned to the application readErr error br *bufio.Reader readRemaining int64 // bytes remaining in current frame. readFinal bool // true the current message has more frames. - readSeq int // incremented to invalidate message readers. readLength int64 // Message size. readLimit int64 // Maximum message size. readMaskPos int readMaskKey [4]byte handlePong func(string) error handlePing func(string) error + handleClose func(int, string) error + readErrCount int + messageReader *messageReader // the current low-level reader + + readDecompress bool // whether last read frame had RSV1 set + newDecompressionReader func(io.Reader) io.ReadCloser } func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { + return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil) +} + +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn { mu := make(chan bool, 1) mu <- true - if readBufferSize == 0 { - readBufferSize = defaultReadBufferSize + var br *bufio.Reader + if readBufferSize == 0 && brw != nil && brw.Reader != nil { + // Reuse the supplied bufio.Reader if the buffer has a useful size. + // This code assumes that peek on a reader returns + // bufio.Reader.buf[:0]. + brw.Reader.Reset(conn) + if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 { + br = brw.Reader + } + } + if br == nil { + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } + if readBufferSize < maxControlFramePayloadSize { + readBufferSize = maxControlFramePayloadSize + } + br = bufio.NewReaderSize(conn, readBufferSize) } - if writeBufferSize == 0 { - writeBufferSize = defaultWriteBufferSize + + var writeBuf []byte + if writeBufferSize == 0 && brw != nil && brw.Writer != nil { + // Use the bufio.Writer's buffer if the buffer has a useful size. This + // code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + brw.Writer.Reset(&wh) + brw.Writer.WriteByte(0) + brw.Flush() + if cap(wh.p) >= maxFrameHeaderSize+256 { + writeBuf = wh.p[:cap(wh.p)] + } + } + + if writeBuf == nil { + if writeBufferSize == 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize) } c := &Conn{ - isServer: isServer, - br: bufio.NewReaderSize(conn, readBufferSize), - conn: conn, - mu: mu, - readFinal: true, - writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize), - writeFrameType: noFrame, - writePos: maxFrameHeaderSize, + isServer: isServer, + br: br, + conn: conn, + mu: mu, + readFinal: true, + writeBuf: writeBuf, + enableWriteCompression: true, + compressionLevel: defaultCompressionLevel, } + c.SetCloseHandler(nil) c.SetPingHandler(nil) c.SetPongHandler(nil) return c @@ -197,7 +342,8 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -// Close closes the underlying network connection without sending or waiting for a close frame. +// Close closes the underlying network connection without sending or waiting +// for a close message. func (c *Conn) Close() error { return c.conn.Close() } @@ -214,28 +360,38 @@ func (c *Conn) RemoteAddr() net.Addr { // Write methods -func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { +func (c *Conn) writeFatal(err error) error { + err = hideTempErr(err) + c.writeErrMu.Lock() + if c.writeErr == nil { + c.writeErr = err + } + c.writeErrMu.Unlock() + return err +} + +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu defer func() { c.mu <- true }() - if c.closeSent { - return ErrCloseSent - } else if frameType == CloseMessage { - c.closeSent = true + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err } c.conn.SetWriteDeadline(deadline) - for _, buf := range bufs { - if len(buf) > 0 { - n, err := c.conn.Write(buf) - if n != len(buf) { - // Close on partial write. - c.conn.Close() - } - if err != nil { - return err - } - } + if len(buf1) == 0 { + _, err = c.conn.Write(buf0) + } else { + err = c.writeBufs(buf0, buf1) + } + if err != nil { + return c.writeFatal(err) + } + if frameType == CloseMessage { + c.writeFatal(ErrCloseSent) } return nil } @@ -285,63 +441,107 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er } defer func() { c.mu <- true }() - if c.closeSent { - return ErrCloseSent - } else if messageType == CloseMessage { - c.closeSent = true + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err } c.conn.SetWriteDeadline(deadline) - n, err := c.conn.Write(buf) - if n != 0 && n != len(buf) { - c.conn.Close() + _, err = c.conn.Write(buf) + if err != nil { + return c.writeFatal(err) + } + if messageType == CloseMessage { + c.writeFatal(ErrCloseSent) } return err } -// NextWriter returns a writer for the next message to send. The writer's -// Close method flushes the complete message to the network. +func (c *Conn) prepWrite(messageType int) error { + // Close previous writer if not already closed by the application. It's + // probably better to return an error in this situation, but we cannot + // change this without breaking existing applications. + if c.writer != nil { + c.writer.Close() + c.writer = nil + } + + if !isControl(messageType) && !isData(messageType) { + return errBadWriteOpCode + } + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + return err +} + +// NextWriter returns a writer for the next message to send. The writer's Close +// method flushes the complete message to the network. // // There can be at most one open writer on a connection. NextWriter closes the // previous writer if the application has not already done so. // -// The NextWriter method and the writers returned from the method cannot be -// accessed by more than one goroutine at a time. +// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and +// PongMessage) are supported. func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { - if c.writeErr != nil { - return nil, c.writeErr + if err := c.prepWrite(messageType); err != nil { + return nil, err } - if c.writeFrameType != noFrame { - if err := c.flushFrame(true, nil); err != nil { - return nil, err - } + mw := &messageWriter{ + c: c, + frameType: messageType, + pos: maxFrameHeaderSize, } - - if !isControl(messageType) && !isData(messageType) { - return nil, errBadWriteOpCode + c.writer = mw + if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { + w := c.newCompressionWriter(c.writer, c.compressionLevel) + mw.compress = true + c.writer = w } + return c.writer, nil +} - c.writeFrameType = messageType - return messageWriter{c, c.writeSeq}, nil +type messageWriter struct { + c *Conn + compress bool // whether next call to flushFrame should set RSV1 + pos int // end of data in writeBuf. + frameType int // type of the current frame. + err error } -func (c *Conn) flushFrame(final bool, extra []byte) error { - length := c.writePos - maxFrameHeaderSize + len(extra) +func (w *messageWriter) fatal(err error) error { + if w.err != nil { + w.err = err + w.c.writer = nil + } + return err +} + +// flushFrame writes buffered data and extra as a frame to the network. The +// final argument indicates that this is the last frame in the message. +func (w *messageWriter) flushFrame(final bool, extra []byte) error { + c := w.c + length := w.pos - maxFrameHeaderSize + len(extra) // Check for invalid control frames. - if isControl(c.writeFrameType) && + if isControl(w.frameType) && (!final || length > maxControlFramePayloadSize) { - c.writeSeq++ - c.writeFrameType = noFrame - c.writePos = maxFrameHeaderSize - return errInvalidControlFrame + return w.fatal(errInvalidControlFrame) } - b0 := byte(c.writeFrameType) + b0 := byte(w.frameType) if final { b0 |= finalBit } + if w.compress { + b0 |= rsv1Bit + } + w.compress = false + b1 := byte(0) if !c.isServer { b1 |= maskBit @@ -373,49 +573,50 @@ func (c *Conn) flushFrame(final bool, extra []byte) error { if !c.isServer { key := newMaskKey() copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) - maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) if len(extra) > 0 { - c.writeErr = errors.New("websocket: internal error, extra used in client mode") - return c.writeErr + return c.writeFatal(errors.New("websocket: internal error, extra used in client mode")) } } - // Write the buffers to the connection. - c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra) + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. - // Setup for next frame. - c.writePos = maxFrameHeaderSize - c.writeFrameType = continuationFrame - if final { - c.writeSeq++ - c.writeFrameType = noFrame + if c.isWriting { + panic("concurrent write to websocket connection") } - return c.writeErr -} + c.isWriting = true -type messageWriter struct { - c *Conn - seq int -} + err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra) -func (w messageWriter) err() error { - c := w.c - if c.writeSeq != w.seq { - return errWriteClosed + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + if err != nil { + return w.fatal(err) } - if c.writeErr != nil { - return c.writeErr + + if final { + c.writer = nil + return nil } + + // Setup for next frame. + w.pos = maxFrameHeaderSize + w.frameType = continuationFrame return nil } -func (w messageWriter) ncopy(max int) (int, error) { - n := len(w.c.writeBuf) - w.c.writePos +func (w *messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.pos if n <= 0 { - if err := w.c.flushFrame(false, nil); err != nil { + if err := w.flushFrame(false, nil); err != nil { return 0, err } - n = len(w.c.writeBuf) - w.c.writePos + n = len(w.c.writeBuf) - w.pos } if n > max { n = max @@ -423,14 +624,14 @@ func (w messageWriter) ncopy(max int) (int, error) { return n, nil } -func (w messageWriter) write(final bool, p []byte) (int, error) { - if err := w.err(); err != nil { - return 0, err +func (w *messageWriter) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err } if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { // Don't buffer large messages. - err := w.c.flushFrame(final, p) + err := w.flushFrame(false, p) if err != nil { return 0, err } @@ -443,20 +644,16 @@ func (w messageWriter) write(final bool, p []byte) (int, error) { if err != nil { return 0, err } - copy(w.c.writeBuf[w.c.writePos:], p[:n]) - w.c.writePos += n + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n p = p[n:] } return nn, nil } -func (w messageWriter) Write(p []byte) (int, error) { - return w.write(false, p) -} - -func (w messageWriter) WriteString(p string) (int, error) { - if err := w.err(); err != nil { - return 0, err +func (w *messageWriter) WriteString(p string) (int, error) { + if w.err != nil { + return 0, w.err } nn := len(p) @@ -465,27 +662,27 @@ func (w messageWriter) WriteString(p string) (int, error) { if err != nil { return 0, err } - copy(w.c.writeBuf[w.c.writePos:], p[:n]) - w.c.writePos += n + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n p = p[n:] } return nn, nil } -func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { - if err := w.err(); err != nil { - return 0, err +func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if w.err != nil { + return 0, w.err } for { - if w.c.writePos == len(w.c.writeBuf) { - err = w.c.flushFrame(false, nil) + if w.pos == len(w.c.writeBuf) { + err = w.flushFrame(false, nil) if err != nil { break } } var n int - n, err = r.Read(w.c.writeBuf[w.c.writePos:]) - w.c.writePos += n + n, err = r.Read(w.c.writeBuf[w.pos:]) + w.pos += n nn += int64(n) if err != nil { if err == io.EOF { @@ -497,30 +694,64 @@ func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { return nn, err } -func (w messageWriter) Close() error { - if err := w.err(); err != nil { +func (w *messageWriter) Close() error { + if w.err != nil { + return w.err + } + if err := w.flushFrame(true, nil); err != nil { + return err + } + w.err = errWriteClosed + return nil +} + +// WritePreparedMessage writes prepared message into connection. +func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error { + frameType, frameData, err := pm.frame(prepareKey{ + isServer: c.isServer, + compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType), + compressionLevel: c.compressionLevel, + }) + if err != nil { return err } - return w.c.flushFrame(true, nil) + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + err = c.write(frameType, c.writeDeadline, frameData, nil) + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + return err } // WriteMessage is a helper method for getting a writer using NextWriter, // writing the message and closing the writer. func (c *Conn) WriteMessage(messageType int, data []byte) error { - wr, err := c.NextWriter(messageType) + + if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) { + // Fast path with no allocations and single frame. + + if err := c.prepWrite(messageType); err != nil { + return err + } + mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize} + n := copy(c.writeBuf[mw.pos:], data) + mw.pos += n + data = data[n:] + return mw.flushFrame(true, data) + } + + w, err := c.NextWriter(messageType) if err != nil { return err } - w := wr.(messageWriter) - if _, err := w.write(true, data); err != nil { + if _, err = w.Write(data); err != nil { return err } - if c.writeSeq == w.seq { - if err := c.flushFrame(true, nil); err != nil { - return err - } - } - return nil + return w.Close() } // SetWriteDeadline sets the write deadline on the underlying network @@ -534,24 +765,7 @@ func (c *Conn) SetWriteDeadline(t time.Time) error { // Read methods -// readFull is like io.ReadFull except that io.EOF is never returned. -func (c *Conn) readFull(p []byte) (err error) { - var n int - for n < len(p) && err == nil { - var nn int - nn, err = c.br.Read(p[n:]) - n += nn - } - if n == len(p) { - err = nil - } else if err == io.EOF { - err = errUnexpectedEOF - } - return -} - func (c *Conn) advanceFrame() (int, error) { - // 1. Skip remainder of previous frame. if c.readRemaining > 0 { @@ -562,19 +776,24 @@ func (c *Conn) advanceFrame() (int, error) { // 2. Read and parse first two bytes of frame header. - var b [8]byte - if err := c.readFull(b[:2]); err != nil { + p, err := c.read(2) + if err != nil { return noFrame, err } - final := b[0]&finalBit != 0 - frameType := int(b[0] & 0xf) - reserved := int((b[0] >> 4) & 0x7) - mask := b[1]&maskBit != 0 - c.readRemaining = int64(b[1] & 0x7f) + final := p[0]&finalBit != 0 + frameType := int(p[0] & 0xf) + mask := p[1]&maskBit != 0 + c.readRemaining = int64(p[1] & 0x7f) - if reserved != 0 { - return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved)) + c.readDecompress = false + if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { + c.readDecompress = true + p[0] &^= rsv1Bit + } + + if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16)) } switch frameType { @@ -603,15 +822,17 @@ func (c *Conn) advanceFrame() (int, error) { switch c.readRemaining { case 126: - if err := c.readFull(b[:2]); err != nil { + p, err := c.read(2) + if err != nil { return noFrame, err } - c.readRemaining = int64(binary.BigEndian.Uint16(b[:2])) + c.readRemaining = int64(binary.BigEndian.Uint16(p)) case 127: - if err := c.readFull(b[:8]); err != nil { + p, err := c.read(8) + if err != nil { return noFrame, err } - c.readRemaining = int64(binary.BigEndian.Uint64(b[:8])) + c.readRemaining = int64(binary.BigEndian.Uint64(p)) } // 4. Handle frame masking. @@ -622,9 +843,11 @@ func (c *Conn) advanceFrame() (int, error) { if mask { c.readMaskPos = 0 - if err := c.readFull(c.readMaskKey[:]); err != nil { + p, err := c.read(len(c.readMaskKey)) + if err != nil { return noFrame, err } + copy(c.readMaskKey[:], p) } // 5. For text and binary messages, enforce read limit and return. @@ -644,9 +867,9 @@ func (c *Conn) advanceFrame() (int, error) { var payload []byte if c.readRemaining > 0 { - payload = make([]byte, c.readRemaining) + payload, err = c.read(int(c.readRemaining)) c.readRemaining = 0 - if err := c.readFull(payload); err != nil { + if err != nil { return noFrame, err } if c.isServer { @@ -666,19 +889,22 @@ func (c *Conn) advanceFrame() (int, error) { return noFrame, err } case CloseMessage: - c.WriteControl(CloseMessage, []byte{}, time.Now().Add(writeWait)) closeCode := CloseNoStatusReceived closeText := "" if len(payload) >= 2 { closeCode = int(binary.BigEndian.Uint16(payload)) + if !isValidReceivedCloseCode(closeCode) { + return noFrame, c.handleProtocolError("invalid close code") + } closeText = string(payload[2:]) + if !utf8.ValidString(closeText) { + return noFrame, c.handleProtocolError("invalid utf8 payload in close frame") + } } - switch closeCode { - case CloseNormalClosure, CloseGoingAway: - return noFrame, io.EOF - default: - return noFrame, &closeError{code: closeCode, text: closeText} + if err := c.handleClose(closeCode, closeText); err != nil { + return noFrame, err } + return noFrame, &CloseError{Code: closeCode, Text: closeText} } return frameType, nil @@ -695,11 +921,18 @@ func (c *Conn) handleProtocolError(message string) error { // There can be at most one open reader on a connection. NextReader discards // the previous message if the application has not already consumed it. // -// The NextReader method and the readers returned from the method cannot be -// accessed by more than one goroutine at a time. +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + // Close previous reader, only relevant for decompression. + if c.reader != nil { + c.reader.Close() + c.reader = nil + } - c.readSeq++ + c.messageReader = nil c.readLength = 0 for c.readErr == nil { @@ -709,59 +942,77 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { break } if frameType == TextMessage || frameType == BinaryMessage { - return frameType, messageReader{c, c.readSeq}, nil + c.messageReader = &messageReader{c} + c.reader = c.messageReader + if c.readDecompress { + c.reader = c.newDecompressionReader(c.reader) + } + return frameType, c.reader, nil } } - return noFrame, nil, c.readErr -} -type messageReader struct { - c *Conn - seq int + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr } -func (r messageReader) Read(b []byte) (int, error) { +type messageReader struct{ c *Conn } - if r.seq != r.c.readSeq { +func (r *messageReader) Read(b []byte) (int, error) { + c := r.c + if c.messageReader != r { return 0, io.EOF } - for r.c.readErr == nil { + for c.readErr == nil { - if r.c.readRemaining > 0 { - if int64(len(b)) > r.c.readRemaining { - b = b[:r.c.readRemaining] + if c.readRemaining > 0 { + if int64(len(b)) > c.readRemaining { + b = b[:c.readRemaining] + } + n, err := c.br.Read(b) + c.readErr = hideTempErr(err) + if c.isServer { + c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) } - n, err := r.c.br.Read(b) - r.c.readErr = hideTempErr(err) - if r.c.isServer { - r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n]) + c.readRemaining -= int64(n) + if c.readRemaining > 0 && c.readErr == io.EOF { + c.readErr = errUnexpectedEOF } - r.c.readRemaining -= int64(n) - return n, r.c.readErr + return n, c.readErr } - if r.c.readFinal { - r.c.readSeq++ + if c.readFinal { + c.messageReader = nil return 0, io.EOF } - frameType, err := r.c.advanceFrame() + frameType, err := c.advanceFrame() switch { case err != nil: - r.c.readErr = hideTempErr(err) + c.readErr = hideTempErr(err) case frameType == TextMessage || frameType == BinaryMessage: - r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") } } - err := r.c.readErr - if err == io.EOF && r.seq == r.c.readSeq { + err := c.readErr + if err == io.EOF && c.messageReader == r { err = errUnexpectedEOF } return 0, err } +func (r *messageReader) Close() error { + return nil +} + // ReadMessage is a helper method for getting a reader using NextReader and // reading from that reader to a buffer. func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { @@ -783,27 +1034,82 @@ func (c *Conn) SetReadDeadline(t time.Time) error { } // SetReadLimit sets the maximum size for a message read from the peer. If a -// message exceeds the limit, the connection sends a close frame to the peer +// message exceeds the limit, the connection sends a close message to the peer // and returns ErrReadLimit to the application. func (c *Conn) SetReadLimit(limit int64) { c.readLimit = limit } +// CloseHandler returns the current close handler +func (c *Conn) CloseHandler() func(code int, text string) error { + return c.handleClose +} + +// SetCloseHandler sets the handler for close messages received from the peer. +// The code argument to h is the received close code or CloseNoStatusReceived +// if the close message is empty. The default close handler sends a close +// message back to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// close messages as described in the section on Control Messages above. +// +// The connection read methods return a CloseError when a close message is +// received. Most applications should handle close messages as part of their +// normal error handling. Applications should only set a close handler when the +// application must perform some action before sending a close message back to +// the peer. +func (c *Conn) SetCloseHandler(h func(code int, text string) error) { + if h == nil { + h = func(code int, text string) error { + message := FormatCloseMessage(code, "") + c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) + return nil + } + } + c.handleClose = h +} + +// PingHandler returns the current ping handler +func (c *Conn) PingHandler() func(appData string) error { + return c.handlePing +} + // SetPingHandler sets the handler for ping messages received from the peer. -// The default ping handler sends a pong to the peer. -func (c *Conn) SetPingHandler(h func(string) error) { +// The appData argument to h is the PING message application data. The default +// ping handler sends a pong to the peer. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// ping messages as described in the section on Control Messages above. +func (c *Conn) SetPingHandler(h func(appData string) error) { if h == nil { h = func(message string) error { - c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) - return nil + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err } } c.handlePing = h } +// PongHandler returns the current pong handler +func (c *Conn) PongHandler() func(appData string) error { + return c.handlePong +} + // SetPongHandler sets the handler for pong messages received from the peer. -// The default pong handler does nothing. -func (c *Conn) SetPongHandler(h func(string) error) { +// The appData argument to h is the PONG message application data. The default +// pong handler does nothing. +// +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// pong messages as described in the section on Control Messages above. +func (c *Conn) SetPongHandler(h func(appData string) error) { if h == nil { h = func(string) error { return nil } } @@ -816,8 +1122,34 @@ func (c *Conn) UnderlyingConn() net.Conn { return c.conn } +// EnableWriteCompression enables and disables write compression of +// subsequent text and binary messages. This function is a noop if +// compression was not negotiated with the peer. +func (c *Conn) EnableWriteCompression(enable bool) { + c.enableWriteCompression = enable +} + +// SetCompressionLevel sets the flate compression level for subsequent text and +// binary messages. This function is a noop if compression was not negotiated +// with the peer. See the compress/flate package for a description of +// compression levels. +func (c *Conn) SetCompressionLevel(level int) error { + if !isValidCompressionLevel(level) { + return errors.New("websocket: invalid compression level") + } + c.compressionLevel = level + return nil +} + // FormatCloseMessage formats closeCode and text as a WebSocket close message. +// An empty message is returned for code CloseNoStatusReceived. func FormatCloseMessage(closeCode int, text string) []byte { + if closeCode == CloseNoStatusReceived { + // Return empty message because it's illegal to send + // CloseNoStatusReceived. Return non-nil value in case application + // checks for nil. + return []byte{} + } buf := make([]byte, 2+len(text)) binary.BigEndian.PutUint16(buf, uint16(closeCode)) copy(buf[2:], text) diff --git a/vendor/github.com/gorilla/websocket/conn_read.go b/vendor/github.com/gorilla/websocket/conn_read.go new file mode 100644 index 000000000..1ea15059e --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_read.go @@ -0,0 +1,18 @@ +// Copyright 2016 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. + +// +build go1.5 + +package websocket + +import "io" + +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} diff --git a/vendor/github.com/gorilla/websocket/conn_read_legacy.go b/vendor/github.com/gorilla/websocket/conn_read_legacy.go new file mode 100644 index 000000000..018541cf6 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_read_legacy.go @@ -0,0 +1,21 @@ +// Copyright 2016 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. + +// +build !go1.5 + +package websocket + +import "io" + +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + if len(p) > 0 { + // advance over the bytes just read + io.ReadFull(c.br, p) + } + return p, err +} diff --git a/vendor/github.com/gorilla/websocket/conn_write.go b/vendor/github.com/gorilla/websocket/conn_write.go new file mode 100644 index 000000000..a509a21f8 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write.go @@ -0,0 +1,15 @@ +// Copyright 2016 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. + +// +build go1.8 + +package websocket + +import "net" + +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err +} diff --git a/vendor/github.com/gorilla/websocket/conn_write_legacy.go b/vendor/github.com/gorilla/websocket/conn_write_legacy.go new file mode 100644 index 000000000..37edaff5a --- /dev/null +++ b/vendor/github.com/gorilla/websocket/conn_write_legacy.go @@ -0,0 +1,18 @@ +// Copyright 2016 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. + +// +build !go1.8 + +package websocket + +func (c *Conn) writeBufs(bufs ...[]byte) error { + for _, buf := range bufs { + if len(buf) > 0 { + if _, err := c.conn.Write(buf); err != nil { + return err + } + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go index f52925dd1..dcce1a63c 100644 --- a/vendor/github.com/gorilla/websocket/doc.go +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -6,9 +6,8 @@ // // Overview // -// The Conn type represents a WebSocket connection. A server application uses -// the Upgrade function from an Upgrader object with a HTTP request handler -// to get a pointer to a Conn: +// The Conn type represents a WebSocket connection. A server application calls +// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn: // // var upgrader = websocket.Upgrader{ // ReadBufferSize: 1024, @@ -31,10 +30,12 @@ // for { // messageType, p, err := conn.ReadMessage() // if err != nil { +// log.Println(err) // return // } -// if err = conn.WriteMessage(messageType, p); err != nil { -// return err +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return // } // } // @@ -46,8 +47,7 @@ // method to get an io.WriteCloser, write the message to the writer and close // the writer when done. To receive a message, call the connection NextReader // method to get an io.Reader and read until io.EOF is returned. This snippet -// snippet shows how to echo messages using the NextWriter and NextReader -// methods: +// shows how to echo messages using the NextWriter and NextReader methods: // // for { // messageType, r, err := conn.NextReader() @@ -86,31 +86,29 @@ // and pong. Call the connection WriteControl, WriteMessage or NextWriter // methods to send a control message to the peer. // -// Connections handle received ping and pong messages by invoking a callback -// function set with SetPingHandler and SetPongHandler methods. These callback -// functions can be invoked from the ReadMessage method, the NextReader method -// or from a call to the data message reader returned from NextReader. -// -// Connections handle received close messages by returning an error from the -// ReadMessage method, the NextReader method or from a call to the data message -// reader returned from NextReader. -// -// Concurrency +// Connections handle received close messages by calling the handler function +// set with the SetCloseHandler method and by returning a *CloseError from the +// NextReader, ReadMessage or the message Read method. The default close +// handler sends a close message to the peer. // -// Connections do not support concurrent calls to the write methods -// (NextWriter, SetWriteDeadline, WriteMessage) or concurrent calls to the read -// methods methods (NextReader, SetReadDeadline, ReadMessage). Connections do -// support a concurrent reader and writer. +// Connections handle received ping messages by calling the handler function +// set with the SetPingHandler method. The default ping handler sends a pong +// message to the peer. // -// The Close and WriteControl methods can be called concurrently with all other -// methods. +// Connections handle received pong messages by calling the handler function +// set with the SetPongHandler method. The default pong handler does nothing. +// If an application sends ping messages, then the application should set a +// pong handler to receive the corresponding pong. // -// Read is Required +// The control message handler functions are called from the NextReader, +// ReadMessage and message reader Read methods. The default close and ping +// handlers can block these methods for a short time when the handler writes to +// the connection. // -// The application must read the connection to process ping and close messages -// sent from the peer. If the application is not otherwise interested in -// messages from the peer, then the application should start a goroutine to read -// and discard messages from the peer. A simple example is: +// The application must read the connection to process close, ping and pong +// messages sent from the peer. If the application is not otherwise interested +// in messages from the peer, then the application should start a goroutine to +// read and discard messages from the peer. A simple example is: // // func readLoop(c *websocket.Conn) { // for { @@ -121,6 +119,20 @@ // } // } // +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and +// that no more than one goroutine calls the read methods (NextReader, +// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler) +// concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// // Origin Considerations // // Web browsers allow Javascript applications to open a WebSocket connection to @@ -132,17 +144,37 @@ // method fails the WebSocket handshake with HTTP status 403. // // If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail -// the handshake if the Origin request header is present and not equal to the -// Host request header. +// the handshake if the Origin request header is present and the Origin host is +// not equal to the Host request header. +// +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. +// +// Compression EXPERIMENTAL +// +// Per message compression extensions (RFC 7692) are experimentally supported +// by this package in a limited capacity. Setting the EnableCompression option +// to true in Dialer or Upgrader will attempt to negotiate per message deflate +// support. +// +// var upgrader = websocket.Upgrader{ +// EnableCompression: true, +// } +// +// If compression was successfully negotiated with the connection's peer, any +// message received in compressed form will be automatically decompressed. +// All Read methods will return uncompressed bytes. +// +// Per message compression of messages written to a connection can be enabled +// or disabled by calling the corresponding Conn method: // -// An application can allow connections from any origin by specifying a -// function that always returns true: +// conn.EnableWriteCompression(false) // -// var upgrader = websocket.Upgrader{ -// CheckOrigin: func(r *http.Request) bool { return true }, -// } +// Currently this package does not support compression with "context takeover". +// This means that messages must be compressed and decompressed in isolation, +// without retaining sliding window or dictionary state across messages. For +// more details refer to RFC 7692. // -// The deprecated Upgrade function does not enforce an origin policy. It's the -// application's responsibility to check the Origin header before calling -// Upgrade. +// Use of compression is experimental and may result in decreased performance. package websocket diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go index 18e62f225..dc2c1f641 100644 --- a/vendor/github.com/gorilla/websocket/json.go +++ b/vendor/github.com/gorilla/websocket/json.go @@ -9,12 +9,14 @@ import ( "io" ) -// WriteJSON is deprecated, use c.WriteJSON instead. +// WriteJSON writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. func WriteJSON(c *Conn, v interface{}) error { return c.WriteJSON(v) } -// WriteJSON writes the JSON encoding of v to the connection. +// WriteJSON writes the JSON encoding of v as a message. // // See the documentation for encoding/json Marshal for details about the // conversion of Go values to JSON. @@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error { return err2 } -// ReadJSON is deprecated, use c.ReadJSON instead. +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// Deprecated: Use c.ReadJSON instead. func ReadJSON(c *Conn, v interface{}) error { return c.ReadJSON(v) } @@ -48,9 +53,7 @@ func (c *Conn) ReadJSON(v interface{}) error { } err = json.NewDecoder(r).Decode(v) if err == io.EOF { - // Decode returns io.EOF when the message is empty or all whitespace. - // Convert to io.ErrUnexpectedEOF so that application can distinguish - // between an error reading the JSON value and the connection closing. + // One value is expected in the message. err = io.ErrUnexpectedEOF } return err diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go new file mode 100644 index 000000000..577fce9ef --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -0,0 +1,54 @@ +// Copyright 2016 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. + +// +build !appengine + +package websocket + +import "unsafe" + +const wordSize = int(unsafe.Sizeof(uintptr(0))) + +func maskBytes(key [4]byte, pos int, b []byte) int { + // Mask one byte at a time for small buffers. + if len(b) < 2*wordSize { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 + } + + // Mask one byte at a time to word boundary. + if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 { + n = wordSize - n + for i := range b[:n] { + b[i] ^= key[pos&3] + pos++ + } + b = b[n:] + } + + // Create aligned word size key. + var k [wordSize]byte + for i := range k { + k[i] = key[(pos+i)&3] + } + kw := *(*uintptr)(unsafe.Pointer(&k)) + + // Mask one word at a time. + n := (len(b) / wordSize) * wordSize + for i := 0; i < n; i += wordSize { + *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw + } + + // Mask one byte at a time for remaining bytes. + b = b[n:] + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + + return pos & 3 +} diff --git a/vendor/github.com/gorilla/websocket/mask_safe.go b/vendor/github.com/gorilla/websocket/mask_safe.go new file mode 100644 index 000000000..2aac060e5 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/mask_safe.go @@ -0,0 +1,15 @@ +// Copyright 2016 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. + +// +build appengine + +package websocket + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go new file mode 100644 index 000000000..1efffbd1e --- /dev/null +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -0,0 +1,103 @@ +// Copyright 2017 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 ( + "bytes" + "net" + "sync" + "time" +) + +// PreparedMessage caches on the wire representations of a message payload. +// Use PreparedMessage to efficiently send a message payload to multiple +// connections. PreparedMessage is especially useful when compression is used +// because the CPU and memory expensive compression operation can be executed +// once for a given set of compression options. +type PreparedMessage struct { + messageType int + data []byte + err error + mu sync.Mutex + frames map[prepareKey]*preparedFrame +} + +// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage. +type prepareKey struct { + isServer bool + compress bool + compressionLevel int +} + +// preparedFrame contains data in wire representation. +type preparedFrame struct { + once sync.Once + data []byte +} + +// NewPreparedMessage returns an initialized PreparedMessage. You can then send +// it to connection using WritePreparedMessage method. Valid wire +// representation will be calculated lazily only once for a set of current +// connection options. +func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) { + pm := &PreparedMessage{ + messageType: messageType, + frames: make(map[prepareKey]*preparedFrame), + data: data, + } + + // Prepare a plain server frame. + _, frameData, err := pm.frame(prepareKey{isServer: true, compress: false}) + if err != nil { + return nil, err + } + + // To protect against caller modifying the data argument, remember the data + // copied to the plain server frame. + pm.data = frameData[len(frameData)-len(data):] + return pm, nil +} + +func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { + pm.mu.Lock() + frame, ok := pm.frames[key] + if !ok { + frame = &preparedFrame{} + pm.frames[key] = frame + } + pm.mu.Unlock() + + var err error + frame.once.Do(func() { + // Prepare a frame using a 'fake' connection. + // TODO: Refactor code in conn.go to allow more direct construction of + // the frame. + mu := make(chan bool, 1) + mu <- true + var nc prepareConn + c := &Conn{ + conn: &nc, + mu: mu, + isServer: key.isServer, + compressionLevel: key.compressionLevel, + enableWriteCompression: true, + writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize), + } + if key.compress { + c.newCompressionWriter = compressNoContextTakeover + } + err = c.WriteMessage(pm.messageType, pm.data) + frame.data = nc.buf.Bytes() + }) + return pm.messageType, frame.data, err +} + +type prepareConn struct { + buf bytes.Buffer + net.Conn +} + +func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) } +func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 000000000..bf2478e43 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -0,0 +1,77 @@ +// Copyright 2017 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 ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +type netDialerFunc func(network, addr string) (net.Conn, error) + +func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { + return fn(network, addr) +} + +func init() { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { + return &httpProxyDialer{proxyURL: proxyURL, fowardDial: forwardDialer.Dial}, nil + }) +} + +type httpProxyDialer struct { + proxyURL *url.URL + fowardDial func(network, addr string) (net.Conn, error) +} + +func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { + hostPort, _ := hostPortNoPort(hpd.proxyURL) + conn, err := hpd.fowardDial(network, hostPort) + if err != nil { + return nil, err + } + + connectHeader := make(http.Header) + if user := hpd.proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: connectHeader, + } + + if err := connectReq.Write(conn); err != nil { + conn.Close() + return nil, err + } + + // Read response. It's OK to use and discard buffered reader here becaue + // the remote server does not speak until spoken to. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + return nil, err + } + + if resp.StatusCode != 200 { + conn.Close() + f := strings.SplitN(resp.Status, " ", 2) + return nil, errors.New(f[1]) + } + return conn, nil +} diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index e56a00493..aee270528 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -28,8 +28,9 @@ type Upgrader struct { HandshakeTimeout time.Duration // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer - // size is zero, then a default value of 4096 is used. The I/O buffer sizes - // do not limit the size of the messages that can be sent or received. + // size is zero, then buffers allocated by the HTTP server are used. The + // I/O buffer sizes do not limit the size of the messages that can be sent + // or received. ReadBufferSize, WriteBufferSize int // Subprotocols specifies the server's supported protocols in order of @@ -43,9 +44,19 @@ type Upgrader struct { Error func(w http.ResponseWriter, r *http.Request, status int, reason error) // CheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. + // CheckOrigin is nil, then a safe default is used: return false if the + // Origin request header is present and the origin host is not equal to + // request Host header. + // + // A CheckOrigin function should carefully validate the request origin to + // prevent cross-site request forgery. CheckOrigin func(r *http.Request) bool + + // EnableCompression specify if the server should attempt to negotiate per + // message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool } func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { @@ -53,6 +64,7 @@ func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status in if u.Error != nil { u.Error(w, r, status, err) } else { + w.Header().Set("Sec-Websocket-Version", "13") http.Error(w, http.StatusText(status), status) } return nil, err @@ -68,7 +80,7 @@ func checkSameOrigin(r *http.Request) bool { if err != nil { return false } - return u.Host == r.Host + return equalASCIIFold(u.Host, r.Host) } func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { @@ -91,18 +103,31 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // // The responseHeader is included in the response to the client's upgrade // request. Use the responseHeader to specify cookies (Set-Cookie) and the -// application negotiated subprotocol (Sec-Websocket-Protocol). +// application negotiated subprotocol (Sec-WebSocket-Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response. func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { - if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" { - return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13") - } + const badHandshake = "websocket: the client is not using the websocket protocol: " if !tokenListContainsValue(r.Header, "Connection", "upgrade") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find connection header with token 'upgrade'") + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header") } if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find upgrade header with token 'websocket'") + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") + } + + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") + } + + if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header") + } + + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") } checkOrigin := u.CheckOrigin @@ -110,19 +135,30 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade checkOrigin = checkSameOrigin } if !checkOrigin(r) { - return u.returnError(w, r, http.StatusForbidden, "websocket: origin not allowed") + return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin") } challengeKey := r.Header.Get("Sec-Websocket-Key") if challengeKey == "" { - return u.returnError(w, r, http.StatusBadRequest, "websocket: key missing or blank") + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank") } subprotocol := u.selectSubprotocol(r, responseHeader) + // Negotiate PMCE + var compress bool + if u.EnableCompression { + for _, ext := range parseExtensions(r.Header) { + if ext[""] != "permessage-deflate" { + continue + } + compress = true + break + } + } + var ( netConn net.Conn - br *bufio.Reader err error ) @@ -130,30 +166,37 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade if !ok { return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") } - var rw *bufio.ReadWriter - netConn, rw, err = h.Hijack() + var brw *bufio.ReadWriter + netConn, brw, err = h.Hijack() if err != nil { return u.returnError(w, r, http.StatusInternalServerError, err.Error()) } - br = rw.Reader - if br.Buffered() > 0 { + if brw.Reader.Buffered() > 0 { netConn.Close() return nil, errors.New("websocket: client sent data before handshake is complete") } - c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize) + c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) c.subprotocol = subprotocol + if compress { + c.newCompressionWriter = compressNoContextTakeover + c.newDecompressionReader = decompressNoContextTakeover + } + p := c.writeBuf[:0] p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, computeAcceptKey(challengeKey)...) p = append(p, "\r\n"...) if c.subprotocol != "" { - p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, "Sec-WebSocket-Protocol: "...) p = append(p, c.subprotocol...) p = append(p, "\r\n"...) } + if compress { + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + } for k, vs := range responseHeader { if k == "Sec-Websocket-Protocol" { continue @@ -193,13 +236,14 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // -// This function is deprecated, use websocket.Upgrader instead. +// Deprecated: Use websocket.Upgrader instead. // -// The application is responsible for checking the request origin before -// calling Upgrade. An example implementation of the same origin policy is: +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: // // if req.Header.Get("Origin") != "http://"+req.Host { -// http.Error(w, "Origin not allowed", 403) +// http.Error(w, "Origin not allowed", http.StatusForbidden) // return // } // @@ -245,3 +289,10 @@ func Subprotocols(r *http.Request) []string { } return protocols } + +// IsWebSocketUpgrade returns true if the client requested upgrade to the +// WebSocket protocol. +func IsWebSocketUpgrade(r *http.Request) bool { + return tokenListContainsValue(r.Header, "Connection", "upgrade") && + tokenListContainsValue(r.Header, "Upgrade", "websocket") +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index ffdc265ed..385fa01be 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -11,21 +11,9 @@ import ( "io" "net/http" "strings" + "unicode/utf8" ) -// tokenListContainsValue returns true if the 1#token header with the given -// name contains token. -func tokenListContainsValue(header http.Header, name string, value string) bool { - for _, v := range header[name] { - for _, s := range strings.Split(v, ",") { - if strings.EqualFold(value, strings.TrimSpace(s)) { - return true - } - } - } - return false -} - var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") func computeAcceptKey(challengeKey string) string { @@ -42,3 +30,208 @@ func generateChallengeKey() (string, error) { } return base64.StdEncoding.EncodeToString(p), nil } + +// Octet types from RFC 2616. +var octetTypes [256]byte + +const ( + isTokenOctet = 1 << iota + isSpaceOctet +) + +func init() { + // From RFC 2616 + // + // OCTET = + // CHAR = + // CTL = + // CR = + // LF = + // SP = + // HT = + // <"> = + // CRLF = CR LF + // LWS = [CRLF] 1*( SP | HT ) + // TEXT = + // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT + // token = 1* + // qdtext = > + + for c := 0; c < 256; c++ { + var t byte + isCtl := c <= 31 || c == 127 + isChar := 0 <= c && c <= 127 + isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 + if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { + t |= isSpaceOctet + } + if isChar && !isCtl && !isSeparator { + t |= isTokenOctet + } + octetTypes[c] = t + } +} + +func skipSpace(s string) (rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isSpaceOctet == 0 { + break + } + } + return s[i:] +} + +func nextToken(s string) (token, rest string) { + i := 0 + for ; i < len(s); i++ { + if octetTypes[s[i]]&isTokenOctet == 0 { + break + } + } + return s[:i], s[i:] +} + +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. +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 +} + +// parseExtensiosn 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 +} diff --git a/vendor/github.com/gorilla/websocket/x_net_proxy.go b/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 000000000..2e668f6b8 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/x_net_proxy.go @@ -0,0 +1,473 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy + +// Package proxy provides support for a variety of protocols to proxy network +// data. +// + +package websocket + +import ( + "errors" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" +) + +type proxy_direct struct{} + +// Direct is a direct proxy: one that makes network connections directly. +var proxy_Direct = proxy_direct{} + +func (proxy_direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type proxy_PerHost struct { + def, bypass proxy_Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost { + return &proxy_PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *proxy_PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *proxy_PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *proxy_PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *proxy_PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *proxy_PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} + +// A Dialer is a means to establish a connection. +type proxy_Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type proxy_Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy related variables in +// the environment. +func proxy_FromEnvironment() proxy_Dialer { + allProxy := proxy_allProxyEnv.Get() + if len(allProxy) == 0 { + return proxy_Direct + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return proxy_Direct + } + proxy, err := proxy_FromURL(proxyURL, proxy_Direct) + if err != nil { + return proxy_Direct + } + + noProxy := proxy_noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := proxy_NewPerHost(proxy, proxy_Direct) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) { + if proxy_proxySchemes == nil { + proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) + } + proxy_proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) { + var auth *proxy_Auth + if u.User != nil { + auth = new(proxy_Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5": + return proxy_SOCKS5("tcp", u.Host, auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxy_proxySchemes != nil { + if f, ok := proxy_proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + proxy_allProxyEnv = &proxy_envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + proxy_noProxyEnv = &proxy_envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type proxy_envOnce struct { + names []string + once sync.Once + val string +} + +func (e *proxy_envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *proxy_envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928 and RFC 1929. +func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) { + s := &proxy_socks5{ + network: network, + addr: addr, + forward: forward, + } + if auth != nil { + s.user = auth.User + s.password = auth.Password + } + + return s, nil +} + +type proxy_socks5 struct { + user, password string + network, addr string + forward proxy_Dialer +} + +const proxy_socks5Version = 5 + +const ( + proxy_socks5AuthNone = 0 + proxy_socks5AuthPassword = 2 +) + +const proxy_socks5Connect = 1 + +const ( + proxy_socks5IP4 = 1 + proxy_socks5Domain = 3 + proxy_socks5IP6 = 4 +) + +var proxy_socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +// Dial connects to the address addr on the given network via the SOCKS5 proxy. +func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + conn, err := s.forward.Dial(s.network, s.addr) + if err != nil { + return nil, err + } + if err := s.connect(conn, addr); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *proxy_socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, proxy_socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + // See RFC 1929 + if buf[1] == proxy_socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, proxy_socks5IP4) + ip = ip4 + } else { + buf = append(buf, proxy_socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination host name too long: " + host) + } + buf = append(buf, proxy_socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(proxy_socks5Errors) { + failure = proxy_socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case proxy_socks5IP4: + bytesToDiscard = net.IPv4len + case proxy_socks5IP6: + bytesToDiscard = net.IPv6len + case proxy_socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} diff --git a/vendor/github.com/mailgun/mailgun-go/bounces.go b/vendor/github.com/mailgun/mailgun-go/bounces.go index 4f82f7c08..d4d2e4ff9 100644 --- a/vendor/github.com/mailgun/mailgun-go/bounces.go +++ b/vendor/github.com/mailgun/mailgun-go/bounces.go @@ -12,16 +12,16 @@ import ( // CreatedAt provides the time at which Mailgun detected the bounce. type Bounce struct { CreatedAt string `json:"created_at"` - code interface{} `json:"code"` + Code interface{} `json:"code"` Address string `json:"address"` Error string `json:"error"` } type Paging struct { - First string `json:"first"` - Next string `json:"next"` - Previous string `json:"previous"` - Last string `json:"last"` + First string `json:"first,omitempty"` + Next string `json:"next,omitempty"` + Previous string `json:"previous,omitempty"` + Last string `json:"last,omitempty"` } type bounceEnvelope struct { @@ -39,7 +39,7 @@ func (i Bounce) GetCreatedAt() (t time.Time, err error) { // returned as a string or as an integer. This method overcomes a protocol // bug in the Mailgun API. func (b Bounce) GetCode() (int, error) { - switch c := b.code.(type) { + switch c := b.Code.(type) { case int: return c, nil case string: diff --git a/vendor/github.com/mailgun/mailgun-go/campaigns.go b/vendor/github.com/mailgun/mailgun-go/campaigns.go index 294ca66d8..6fe151576 100644 --- a/vendor/github.com/mailgun/mailgun-go/campaigns.go +++ b/vendor/github.com/mailgun/mailgun-go/campaigns.go @@ -3,17 +3,17 @@ package mailgun // Campaigns have been deprecated since development work on this SDK commenced. // Please refer to http://documentation.mailgun.com/api_reference . type Campaign struct { - Id string `json:"id"` - Name string `json:"name"` - CreatedAt string `json:"created_at"` - DeliveredCount int `json:"delivered_count"` - ClickedCount int `json:"clicked_count"` - OpenedCount int `json:"opened_count"` - SubmittedCount int `json:"submitted_count"` - UnsubscribedCount int `json:"unsubscribed_count"` - BouncedCount int `json:"bounced_count"` - ComplainedCount int `json:"complained_count"` - DroppedCount int `json:"dropped_count"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + DeliveredCount int `json:"delivered_count,omitempty"` + ClickedCount int `json:"clicked_count,omitempty"` + OpenedCount int `json:"opened_count,omitempty"` + SubmittedCount int `json:"submitted_count,omitempty"` + UnsubscribedCount int `json:"unsubscribed_count,omitempty"` + BouncedCount int `json:"bounced_count,omitempty"` + ComplainedCount int `json:"complained_count,omitempty"` + DroppedCount int `json:"dropped_count,omitempty"` } type campaignsEnvelope struct { diff --git a/vendor/github.com/mailgun/mailgun-go/domains.go b/vendor/github.com/mailgun/mailgun-go/domains.go index a801e0684..4db82dbd2 100644 --- a/vendor/github.com/mailgun/mailgun-go/domains.go +++ b/vendor/github.com/mailgun/mailgun-go/domains.go @@ -5,7 +5,7 @@ import ( "time" ) -// DefaultLimit and DefaultSkip instruct the SDK to rely on Mailgun's reasonable defaults for pagination settings. +// DefaultLimit and DefaultSkip instruct the SDK to rely on Mailgun's reasonable defaults for Paging settings. const ( DefaultLimit = -1 DefaultSkip = -1 @@ -66,7 +66,7 @@ func (d Domain) GetCreatedAt() (t time.Time, err error) { // Note that zero items and a zero-length slice do not necessarily imply an error occurred. // Except for the error itself, all results are undefined in the event of an error. func (m *MailgunImpl) GetDomains(limit, skip int) (int, []Domain, error) { - r := newHTTPRequest(generatePublicApiUrl(domainsEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(m, domainsEndpoint)) r.setClient(m.Client()) if limit != DefaultLimit { r.addParameter("limit", strconv.Itoa(limit)) @@ -86,7 +86,7 @@ func (m *MailgunImpl) GetDomains(limit, skip int) (int, []Domain, error) { // Retrieve detailed information about the named domain. func (m *MailgunImpl) GetSingleDomain(domain string) (Domain, []DNSRecord, []DNSRecord, error) { - r := newHTTPRequest(generatePublicApiUrl(domainsEndpoint) + "/" + domain) + r := newHTTPRequest(generatePublicApiUrl(m, domainsEndpoint) + "/" + domain) r.setClient(m.Client()) r.setBasicAuth(basicAuthUser, m.ApiKey()) var envelope singleDomainEnvelope @@ -101,7 +101,7 @@ func (m *MailgunImpl) GetSingleDomain(domain string) (Domain, []DNSRecord, []DNS // The wildcard parameter instructs Mailgun to treat all subdomains of this domain uniformly if true, // and as different domains if false. func (m *MailgunImpl) CreateDomain(name string, smtpPassword string, spamAction string, wildcard bool) error { - r := newHTTPRequest(generatePublicApiUrl(domainsEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(m, domainsEndpoint)) r.setClient(m.Client()) r.setBasicAuth(basicAuthUser, m.ApiKey()) @@ -116,7 +116,7 @@ func (m *MailgunImpl) CreateDomain(name string, smtpPassword string, spamAction // DeleteDomain instructs Mailgun to dispose of the named domain name. func (m *MailgunImpl) DeleteDomain(name string) error { - r := newHTTPRequest(generatePublicApiUrl(domainsEndpoint) + "/" + name) + r := newHTTPRequest(generatePublicApiUrl(m, domainsEndpoint) + "/" + name) r.setClient(m.Client()) r.setBasicAuth(basicAuthUser, m.ApiKey()) _, err := makeDeleteRequest(r) diff --git a/vendor/github.com/mailgun/mailgun-go/email_validation.go b/vendor/github.com/mailgun/mailgun-go/email_validation.go index 883625caa..8af703f9c 100644 --- a/vendor/github.com/mailgun/mailgun-go/email_validation.go +++ b/vendor/github.com/mailgun/mailgun-go/email_validation.go @@ -24,11 +24,16 @@ type EmailVerificationParts struct { // Mailgun thinks you might have a typo. // DidYouMean may be empty (""), in which case Mailgun has no recommendation to give. // The existence of DidYouMean does NOT imply the email provided has anything wrong with it. +// IsDisposableAddress indicates whether Mailgun thinks the address is from a known +// disposable mailbox provider. +// IsRoleAddress indicates whether Mailgun thinks the address is an email distribution list. type EmailVerification struct { - IsValid bool `json:"is_valid"` - Parts EmailVerificationParts `json:"parts"` - Address string `json:"address"` - DidYouMean string `json:"did_you_mean"` + IsValid bool `json:"is_valid"` + Parts EmailVerificationParts `json:"parts"` + Address string `json:"address"` + DidYouMean string `json:"did_you_mean"` + IsDisposableAddress bool `json:"is_disposable_address"` + IsRoleAddress bool `json:"is_role_address"` } type addressParseResult struct { @@ -40,7 +45,7 @@ type addressParseResult struct { // It may also be used to break an email address into its sub-components. (See example.) // NOTE: Use of this function requires a proper public API key. The private API key will not work. func (m *MailgunImpl) ValidateEmail(email string) (EmailVerification, error) { - r := newHTTPRequest(generatePublicApiUrl(addressValidateEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(m, addressValidateEndpoint)) r.setClient(m.Client()) r.addParameter("address", email) r.setBasicAuth(basicAuthUser, m.PublicApiKey()) @@ -57,7 +62,7 @@ func (m *MailgunImpl) ValidateEmail(email string) (EmailVerification, error) { // ParseAddresses takes a list of addresses and sorts them into valid and invalid address categories. // NOTE: Use of this function requires a proper public API key. The private API key will not work. func (m *MailgunImpl) ParseAddresses(addresses ...string) ([]string, []string, error) { - r := newHTTPRequest(generatePublicApiUrl(addressParseEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(m, addressParseEndpoint)) r.setClient(m.Client()) r.addParameter("addresses", strings.Join(addresses, ",")) r.setBasicAuth(basicAuthUser, m.PublicApiKey()) diff --git a/vendor/github.com/mailgun/mailgun-go/enums.go b/vendor/github.com/mailgun/mailgun-go/enums.go new file mode 100644 index 000000000..d708bc0bb --- /dev/null +++ b/vendor/github.com/mailgun/mailgun-go/enums.go @@ -0,0 +1,336 @@ +package mailgun + +import ( + "encoding/json" + "fmt" + "net" + "strings" + "time" +) + +type EventType uint8 + +const ( + EventUnknown EventType = iota + EventAccepted + EventRejected + EventDelivered + EventFailed + EventOpened + EventClicked + EventUnsubscribed + EventComplained + EventStored + EventDropped +) + +var eventTypes = []string{ + "unknown", + "accepted", + "rejected", + "delivered", + "failed", + "opened", + "clicked", + "unsubscribed", + "complained", + "stored", + "dropped", +} + +func (et EventType) String() string { + return eventTypes[et] +} + +// MarshalText satisfies TextMarshaler +func (et EventType) MarshalText() ([]byte, error) { + return []byte(et.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (et *EventType) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(eventTypes); i++ { + if enum == eventTypes[i] { + *et = EventType(i) + return nil + } + } + return fmt.Errorf("unknown event type '%s'", enum) +} + +type TimestampNano time.Time + +// MarshalText satisfies JSONMarshaler +func (tn TimestampNano) MarshalJSON() ([]byte, error) { + t := time.Time(tn) + v := float64(t.Unix()) + float64(t.Nanosecond())/float64(time.Nanosecond) + return json.Marshal(v) +} + +// UnmarshalText satisfies JSONUnmarshaler +func (tn *TimestampNano) UnmarshalJSON(data []byte) error { + var v float64 + err := json.Unmarshal(data, &v) + if err == nil { + *tn = TimestampNano(time.Unix(0, int64(v*float64(time.Second)))) + } + return err +} + +type IP net.IP + +// MarshalText satisfies TextMarshaler +func (i IP) MarshalText() ([]byte, error) { + return []byte(net.IP(i).String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (i *IP) UnmarshalText(text []byte) error { + s := strings.Trim(string(text), "\"") + v := net.ParseIP(s) + if v != nil { + *i = IP(v) + } + return nil +} + +type Method uint8 + +const ( + MethodUnknown Method = iota + MethodSMTP + MethodHTTP +) + +var methods = []string{ + "unknown", + "smtp", + "http", +} + +func (m Method) String() string { + return methods[m] +} + +// MarshalText satisfies TextMarshaler +func (m Method) MarshalText() ([]byte, error) { + return []byte(m.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (m *Method) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(methods); i++ { + if enum == methods[i] { + *m = Method(i) + return nil + } + } + return fmt.Errorf("unknown event method '%s'", enum) +} + +type EventSeverity uint8 + +const ( + SeverityUnknown EventSeverity = iota + SeverityTemporary + SeverityPermanent + SeverityInternal +) + +var severities = []string{ + "unknown", + "temporary", + "permanent", + "internal", +} + +func (es EventSeverity) String() string { + return severities[es] +} + +// MarshalText satisfies TextMarshaler +func (es EventSeverity) MarshalText() ([]byte, error) { + return []byte(es.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (es *EventSeverity) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(severities); i++ { + if enum == severities[i] { + *es = EventSeverity(i) + return nil + } + } + return fmt.Errorf("unknown event severity '%s'", enum) +} + +type EventReason uint8 + +const ( + ReasonUnknown EventReason = iota + ReasonGeneric + ReasonBounce + ReasonESPBlock + ReasonGreylisted + ReasonBlacklisted + ReasonSuppressBounce + ReasonSuppressComplaint + ReasonSuppressUnsubscribe + ReasonOld + ReasonHardFail +) + +var eventReasons = []string{ + "unknown", + "generic", + "bounce", + "espblock", + "greylisted", + "blacklisted", + "suppress-bounce", + "suppress-complaint", + "suppress-unsubscribe", + "old", + "hardfail", +} + +func (er EventReason) String() string { + return eventReasons[er] +} + +// MarshalText satisfies TextMarshaler +func (er EventReason) MarshalText() ([]byte, error) { + return []byte(er.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (er *EventReason) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(eventReasons); i++ { + if enum == eventReasons[i] { + *er = EventReason(i) + return nil + } + } + return fmt.Errorf("unknown event reason '%s'", enum) +} + +type ClientType uint + +const ( + ClientUnknown ClientType = iota + ClientMobileBrowser + ClientBrowser + ClientEmail + ClientLibrary + ClientRobot + ClientOther +) + +var clientTypes = []string{ + "unknown", + "mobile browser", + "browser", + "email client", + "library", + "robot", + "other", +} + +func (ct ClientType) String() string { + return clientTypes[ct] +} + +// MarshalText satisfies TextMarshaler +func (ct ClientType) MarshalText() ([]byte, error) { + return []byte(ct.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (ct *ClientType) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(clientTypes); i++ { + if enum == clientTypes[i] { + *ct = ClientType(i) + return nil + } + } + return fmt.Errorf("unknown client type '%s'", enum) +} + +type DeviceType uint + +const ( + DeviceUnknown DeviceType = iota + DeviceMobileBrowser + DeviceBrowser + DeviceEmail + DeviceOther +) + +var deviceTypes = []string{ + "unknown", + "desktop", + "mobile", + "tablet", + "other", +} + +func (ct DeviceType) String() string { + return deviceTypes[ct] +} + +// MarshalText satisfies TextMarshaler +func (ct DeviceType) MarshalText() ([]byte, error) { + return []byte(ct.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (ct *DeviceType) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(deviceTypes); i++ { + if enum == deviceTypes[i] { + *ct = DeviceType(i) + return nil + } + } + return fmt.Errorf("unknown device type '%s'", enum) +} + +type TransportMethod uint + +const ( + TransportUnknown TransportMethod = iota + TransportHTTP + TransportSMTP +) + +var transportMethods = []string{ + "unknown", + "http", + "smtp", +} + +func (tm TransportMethod) String() string { + return transportMethods[tm] +} + +// MarshalText satisfies TextMarshaler +func (tm TransportMethod) MarshalText() ([]byte, error) { + return []byte(tm.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (tm *TransportMethod) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(transportMethods); i++ { + if enum == transportMethods[i] { + *tm = TransportMethod(i) + return nil + } + } + return fmt.Errorf("unknown transport method '%s'", enum) +} diff --git a/vendor/github.com/mailgun/mailgun-go/events.go b/vendor/github.com/mailgun/mailgun-go/events.go index 59b7b54db..e608f25ec 100644 --- a/vendor/github.com/mailgun/mailgun-go/events.go +++ b/vendor/github.com/mailgun/mailgun-go/events.go @@ -3,15 +3,116 @@ package mailgun import ( "fmt" "time" + + "github.com/pkg/errors" ) -// Events are open-ended, loosely-defined JSON documents. -// They will always have an event and a timestamp field, however. -type Event map[string]interface{} +type eventResponse struct { + Events []Event `json:"items"` + Paging Paging `json:"paging"` +} + +type Event struct { + // Mandatory fields present in each event + ID string `json:"id"` + Timestamp TimestampNano `json:"timestamp"` + Event EventType `json:"event"` + + // Delivery related values + DeliveryStatus *DeliveryStatus `json:"delivery-status,omitempty"` + Reason *EventReason `json:"reason,omitempty"` + Severity *EventSeverity `json:"severity,omitempty"` + + // Message classification / grouping + Tags []string `json:"tags,omitempty"` + Campaigns []Campaign `json:"campaigns,omitempty"` + + // Recipient information (for recipient-initiated events: opens, clicks etc) + ClientInfo *ClientInfo `json:"client-info,omitempty"` + Geolocation *Geolocation `json:"geolocation,omitempty"` + IP *IP `json:"ip,omitempty"` + Envelope *Envelope `json:"envelope,omitempty"` + + // Clicked + URL *string `json:"url,omitempty"` + + // Message + // TODO: unify message types + Message *EventMessage `json:"message,omitempty"` + Batch *Batch `json:"batch,omitempty"` + Recipient *Recipient `json:"recipient,omitempty"` + Routes []Route `json:"routes,omitempty"` + Storage *Storage `json:"storage,omitempty"` + UserVariables map[string]string `json:"user-variables"` + + // API + Method *Method `json:"method,omitempty"` + Flags *EventFlags `json:"flags,omitempty"` +} + +type DeliveryStatus struct { + Message *string `json:"message,omitempty"` + Code interface{} `json:"code,omitempty"` + Description *string `json:"description,omitempty"` + Retry *int `json:"retry-seconds,omitempty"` +} + +type EventFlags struct { + Authenticated bool `json:"is-authenticated"` + Batch bool `json:"is-batch"` + Big bool `json:"is-big"` + Callback bool `json:"is-callback"` + DelayedBounce bool `json:"is-delayed-bounce"` + SystemTest bool `json:"is-system-test"` + TestMode bool `json:"is-test-mode"` +} + +type ClientInfo struct { + ClientType *ClientType `json:"client-type,omitempty"` + ClientOS *string `json:"client-os,omitempty"` + ClientName *string `json:"client-name,omitempty"` + DeviceType *DeviceType `json:"device-type,omitempty"` + UserAgent *string `json:"user-agent,omitempty"` +} + +type Geolocation struct { + Country *string `json:"country,omitempty"` + Region *string `json:"region,omitempty"` + City *string `json:"city,omitempty"` +} + +type Storage struct { + URL string `json:"url"` + Key string `json:"key"` +} + +type Batch struct { + ID string `json:"id"` +} -// noTime always equals an uninitialized Time structure. -// It's used to detect when a time parameter is provided. -var noTime time.Time +type Envelope struct { + Sender *string `json:"sender,omitempty"` + SendingHost *string `json:"sending-host,omitempty"` + SendingIP *IP `json:"sending-ip,omitempty"` + Targets *string `json:"targets,omitempty"` + Transport *TransportMethod `json:"transport,omitempty"` +} + +type EventMessage struct { + Headers map[string]string `json:"headers,omitempty"` + Recipients []string `json:"recipients,omitempty"` + Attachments []StoredAttachment `json:"attachments,omitempty"` + Size *int `json:"size,omitempty"` +} + +func (em *EventMessage) ID() (string, error) { + if em != nil && em.Headers != nil { + if id, ok := em.Headers["message-id"]; ok { + return id, nil + } + } + return "", errors.New("message id not set") +} // GetEventsOptions lets the caller of GetEvents() specify how the results are to be returned. // Begin and End time-box the results returned. @@ -23,8 +124,18 @@ var noTime time.Time // Otherwise, the JSON is spaced appropriately for human consumption. // Filter allows the caller to provide more specialized filters on the query. // Consult the Mailgun documentation for more details. +type EventsOptions struct { + Begin, End *time.Time + ForceAscending, ForceDescending, Compact bool + Limit int + Filter map[string]string + ThresholdAge time.Duration + PollInterval time.Duration +} + +// Depreciated See `ListEvents()` type GetEventsOptions struct { - Begin, End time.Time + Begin, End *time.Time ForceAscending, ForceDescending, Compact bool Limit int Filter map[string]string @@ -32,22 +143,68 @@ type GetEventsOptions struct { // EventIterator maintains the state necessary for paging though small parcels of a larger set of events. type EventIterator struct { - events []Event - nextURL, prevURL string - mg Mailgun + eventResponse + mg Mailgun + err error } // NewEventIterator creates a new iterator for events. // Use GetFirstPage to retrieve the first batch of events. -// Use Next and Previous thereafter as appropriate to iterate through sets of data. +// Use GetNext and GetPrevious thereafter as appropriate to iterate through sets of data. +// +// *This call is Deprecated, use ListEvents() instead* func (mg *MailgunImpl) NewEventIterator() *EventIterator { return &EventIterator{mg: mg} } -// Events returns the most recently retrieved batch of events. -// The length is guaranteed to fall between 0 and the limit set in the GetEventsOptions structure passed to GetFirstPage. -func (ei *EventIterator) Events() []Event { - return ei.events +// Create an new iterator to fetch a page of events from the events api +// it := mg.ListEvents(EventsOptions{}) +// var events []Event +// for it.Next(&events) { +// for _, event := range events { +// // Do things with events +// } +// } +// if it.Err() != nil { +// log.Fatal(it.Err()) +// } +func (mg *MailgunImpl) ListEvents(opts *EventsOptions) *EventIterator { + req := newHTTPRequest(generateApiUrl(mg, eventsEndpoint)) + if opts != nil { + if opts.Limit > 0 { + req.addParameter("limit", fmt.Sprintf("%d", opts.Limit)) + } + if opts.Compact { + req.addParameter("pretty", "no") + } + if opts.ForceAscending { + req.addParameter("ascending", "yes") + } else if opts.ForceDescending { + req.addParameter("ascending", "no") + } + if opts.Begin != nil { + req.addParameter("begin", formatMailgunTime(opts.Begin)) + } + if opts.End != nil { + req.addParameter("end", formatMailgunTime(opts.End)) + } + if opts.Filter != nil { + for k, v := range opts.Filter { + req.addParameter(k, v) + } + } + } + url, err := req.generateUrlWithParameters() + return &EventIterator{ + mg: mg, + eventResponse: eventResponse{Paging: Paging{Next: url, First: url}}, + err: err, + } +} + +// If an error occurred during iteration `Err()` will return non nil +func (ei *EventIterator) Err() error { + return ei.err } // GetFirstPage retrieves the first batch of events, according to your criteria. @@ -70,11 +227,11 @@ func (ei *EventIterator) GetFirstPage(opts GetEventsOptions) error { if opts.ForceDescending { payload.addValue("ascending", "no") } - if opts.Begin != noTime { - payload.addValue("begin", formatMailgunTime(&opts.Begin)) + if opts.Begin != nil { + payload.addValue("begin", formatMailgunTime(opts.Begin)) } - if opts.End != noTime { - payload.addValue("end", formatMailgunTime(&opts.End)) + if opts.End != nil { + payload.addValue("end", formatMailgunTime(opts.End)) } if opts.Filter != nil { for k, v := range opts.Filter { @@ -92,13 +249,189 @@ func (ei *EventIterator) GetFirstPage(opts GetEventsOptions) error { // Retrieves the chronologically previous batch of events, if any exist. // You know you're at the end of the list when len(Events())==0. func (ei *EventIterator) GetPrevious() error { - return ei.fetch(ei.prevURL) + return ei.fetch(ei.Paging.Previous) } // Retrieves the chronologically next batch of events, if any exist. // You know you're at the end of the list when len(Events())==0. func (ei *EventIterator) GetNext() error { - return ei.fetch(ei.nextURL) + return ei.fetch(ei.Paging.Next) +} + +// Retrieves the next page of events from the api. Returns false when there +// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve +// the error +func (ei *EventIterator) Next(events *[]Event) bool { + if ei.err != nil { + return false + } + ei.err = ei.fetch(ei.Paging.Next) + if ei.err != nil { + return false + } + *events = ei.Events + if len(ei.Events) == 0 { + return false + } + return true +} + +// Retrieves the first page of events from the api. Returns false if there +// was an error. It also sets the iterator object to the first page. +// Use `.Err()` to retrieve the error. +func (ei *EventIterator) First(events *[]Event) bool { + if ei.err != nil { + return false + } + ei.err = ei.fetch(ei.Paging.First) + if ei.err != nil { + return false + } + *events = ei.Events + return true +} + +// Retrieves the last page of events from the api. +// Calling Last() is invalid unless you first call First() or Next() +// Returns false if there was an error. It also sets the iterator object +// to the last page. Use `.Err()` to retrieve the error. +func (ei *EventIterator) Last(events *[]Event) bool { + if ei.err != nil { + return false + } + ei.err = ei.fetch(ei.Paging.Last) + if ei.err != nil { + return false + } + *events = ei.Events + return true +} + +// Retrieves the previous page of events from the api. Returns false when there +// no more pages to retrieve or if there was an error. Use `.Err()` to retrieve +// the error if any +func (ei *EventIterator) Previous(events *[]Event) bool { + if ei.err != nil { + return false + } + if ei.Paging.Previous == "" { + return false + } + ei.err = ei.fetch(ei.Paging.Previous) + if ei.err != nil { + return false + } + *events = ei.Events + if len(ei.Events) == 0 { + return false + } + return true +} + +// EventPoller maintains the state necessary for polling events +type EventPoller struct { + it *EventIterator + opts EventsOptions + thresholdTime time.Time + sleepUntil time.Time + mg Mailgun + err error +} + +// Poll the events api and return new events as they occur +// it = mg.PollEvents(&EventsOptions{ +// // Poll() returns after this threshold is met, or events older than this threshold appear +// ThresholdAge: time.Second * 10, +// // Only events with a timestamp after this date/time will be returned +// Begin: time.Now().Add(time.Second * -3), +// // How often we poll the api for new events +// PollInterval: time.Second * 4}) +// var events []Event +// // Blocks until new events appear +// for it.Poll(&events) { +// for _, event := range(events) { +// fmt.Printf("Event %+v\n", event) +// } +// } +// if it.Err() != nil { +// log.Fatal(it.Err()) +// } +func (mg *MailgunImpl) PollEvents(opts *EventsOptions) *EventPoller { + now := time.Now() + // ForceAscending must be set + opts.ForceAscending = true + + // Default begin time is 30 minutes ago + if opts.Begin == nil { + t := now.Add(time.Minute * -30) + opts.Begin = &t + } + + // Default threshold age is 30 minutes + if opts.ThresholdAge.Nanoseconds() == 0 { + opts.ThresholdAge = time.Duration(time.Minute * 30) + } + + // Set a 15 second poll interval if none set + if opts.PollInterval.Nanoseconds() == 0 { + opts.PollInterval = time.Duration(time.Second * 15) + } + + return &EventPoller{ + it: mg.ListEvents(opts), + opts: *opts, + mg: mg, + } +} + +// If an error occurred during polling `Err()` will return non nil +func (ep *EventPoller) Err() error { + return ep.err +} + +func (ep *EventPoller) Poll(events *[]Event) bool { + var currentPage string + ep.thresholdTime = time.Now().UTC().Add(ep.opts.ThresholdAge) + for { + if !ep.sleepUntil.IsZero() { + // Sleep the rest of our duration + time.Sleep(ep.sleepUntil.Sub(time.Now())) + } + + // Remember our current page url + currentPage = ep.it.Paging.Next + + // Attempt to get a page of events + var page []Event + if ep.it.Next(&page) == false { + if ep.it.Err() == nil && len(page) == 0 { + // No events, sleep for our poll interval + ep.sleepUntil = time.Now().Add(ep.opts.PollInterval) + continue + } + ep.err = ep.it.Err() + return false + } + + // Last event on the page + lastEvent := page[len(page)-1] + + timeStamp := time.Time(lastEvent.Timestamp) + // Record the next time we should query for new events + ep.sleepUntil = time.Now().Add(ep.opts.PollInterval) + + // If the last event on the page is older than our threshold time + // or we have been polling for longer than our threshold time + if timeStamp.After(ep.thresholdTime) || time.Now().UTC().After(ep.thresholdTime) { + ep.thresholdTime = time.Now().UTC().Add(ep.opts.ThresholdAge) + // Return the page of events to the user + *events = page + return true + } + // Since we didn't find an event older than our + // threshold, fetch this same page again + ep.it.Paging.Next = currentPage + } } // GetFirstPage, GetPrevious, and GetNext all have a common body of code. @@ -107,24 +440,6 @@ func (ei *EventIterator) fetch(url string) error { r := newHTTPRequest(url) r.setClient(ei.mg.Client()) r.setBasicAuth(basicAuthUser, ei.mg.ApiKey()) - var response map[string]interface{} - err := getResponseFromJSON(r, &response) - if err != nil { - return err - } - items := response["items"].([]interface{}) - ei.events = make([]Event, len(items)) - for i, item := range items { - ei.events[i] = item.(map[string]interface{}) - } - - pagings := response["paging"].(map[string]interface{}) - links := make(map[string]string, len(pagings)) - for key, page := range pagings { - links[key] = page.(string) - } - ei.nextURL = links["next"] - ei.prevURL = links["previous"] - return err + return getResponseFromJSON(r, &ei.eventResponse) } diff --git a/vendor/github.com/mailgun/mailgun-go/httphelpers.go b/vendor/github.com/mailgun/mailgun-go/httphelpers.go index 20e03ab05..790c86a41 100644 --- a/vendor/github.com/mailgun/mailgun-go/httphelpers.go +++ b/vendor/github.com/mailgun/mailgun-go/httphelpers.go @@ -3,6 +3,7 @@ package mailgun import ( "bytes" "encoding/json" + "fmt" "io" "io/ioutil" "mime/multipart" @@ -10,6 +11,7 @@ import ( "net/url" "os" "path" + "strings" ) type httpRequest struct { @@ -29,6 +31,7 @@ type httpResponse struct { type payload interface { getPayloadBuffer() (*bytes.Buffer, error) getContentType() string + getValues() []keyValuePair } type keyValuePair struct { @@ -93,6 +96,10 @@ func (f *urlEncodedPayload) getContentType() string { return "application/x-www-form-urlencoded" } +func (f *urlEncodedPayload) getValues() []keyValuePair { + return f.Values +} + func (r *httpResponse) parseFromJSON(v interface{}) error { return json.Unmarshal(r.Data, v) } @@ -101,6 +108,10 @@ func newFormDataPayload() *formDataPayload { return &formDataPayload{} } +func (f *formDataPayload) getValues() []keyValuePair { + return f.Values +} + func (f *formDataPayload) addValue(key, value string) { f.Values = append(f.Values, keyValuePair{key: key, value: value}) } @@ -215,6 +226,10 @@ func (r *httpRequest) makeRequest(method string, payload payload) (*httpResponse req.Header.Add(header, value) } + if Debug { + fmt.Println(r.curlString(req, payload)) + } + response := httpResponse{} resp, err := r.Client.Do(req) @@ -252,3 +267,20 @@ func (r *httpRequest) generateUrlWithParameters() (string, error) { return url.String(), nil } + +func (r *httpRequest) curlString(req *http.Request, p payload) string { + + parts := []string{"curl", "-i", "-X", req.Method, req.URL.String()} + for key, value := range req.Header { + parts = append(parts, fmt.Sprintf("-H \"%s: %s\"", key, value[0])) + } + + //parts = append(parts, fmt.Sprintf(" --user '%s:%s'", r.BasicAuthUser, r.BasicAuthPassword)) + + if p != nil { + for _, param := range p.getValues() { + parts = append(parts, fmt.Sprintf(" -F %s='%s'", param.key, param.value)) + } + } + return strings.Join(parts, " ") +} diff --git a/vendor/github.com/mailgun/mailgun-go/mailgun.go b/vendor/github.com/mailgun/mailgun-go/mailgun.go index 78a1f544e..1c878d99f 100644 --- a/vendor/github.com/mailgun/mailgun-go/mailgun.go +++ b/vendor/github.com/mailgun/mailgun-go/mailgun.go @@ -93,22 +93,27 @@ package mailgun import ( + "errors" "fmt" "io" "net/http" + "os" "time" ) +var Debug = false + const ( - apiBase = "https://api.mailgun.net/v3" + ApiBase = "https://api.mailgun.net/v3" messagesEndpoint = "messages" mimeMessagesEndpoint = "messages.mime" addressValidateEndpoint = "address/validate" addressParseEndpoint = "address/parse" bouncesEndpoint = "bounces" statsEndpoint = "stats" + statsTotalEndpoint = "stats/total" domainsEndpoint = "domains" - deleteTagEndpoint = "tags" + tagsEndpoint = "tags" campaignsEndpoint = "campaigns" eventsEndpoint = "events" credentialsEndpoint = "credentials" @@ -128,6 +133,7 @@ const ( // Always double-check with the Mailgun API Documentation to // determine the currently supported feature set. type Mailgun interface { + ApiBase() string Domain() string ApiKey() string PublicApiKey() string @@ -141,7 +147,10 @@ type Mailgun interface { AddBounce(address, code, error string) error DeleteBounce(address string) error GetStats(limit int, skip int, startDate *time.Time, event ...string) (int, []Stat, error) + GetStatsTotal(start *time.Time, end *time.Time, resolution string, duration string, event ...string) (*StatsTotalResponse, error) + GetTag(tag string) (TagItem, error) DeleteTag(tag string) error + ListTags(*TagOptions) *TagIterator GetDomains(limit, skip int) (int, []Domain, error) GetSingleDomain(domain string) (Domain, []DNSRecord, []DNSRecord, error) CreateDomain(name string, smtpPassword string, spamAction string, wildcard bool) error @@ -154,6 +163,8 @@ type Mailgun interface { GetSingleComplaint(address string) (Complaint, error) GetStoredMessage(id string) (StoredMessage, error) GetStoredMessageRaw(id string) (StoredMessageRaw, error) + GetStoredMessageForURL(url string) (StoredMessage, error) + GetStoredMessageRawForURL(url string) (StoredMessageRaw, error) DeleteStoredMessage(id string) error GetCredentials(limit, skip int) (int, []Credential, error) CreateCredential(login, password string) error @@ -176,6 +187,7 @@ type Mailgun interface { DeleteWebhook(kind string) error GetWebhookByType(kind string) (string, error) UpdateWebhook(kind, url string) error + VerifyWebhookRequest(req *http.Request) (verified bool, err error) GetLists(limit, skip int, filter string) (int, []List, error) CreateList(List) (List, error) DeleteList(string) error @@ -190,20 +202,26 @@ type Mailgun interface { NewMessage(from, subject, text string, to ...string) *Message NewMIMEMessage(body io.ReadCloser, to ...string) *Message NewEventIterator() *EventIterator + ListEvents(*EventsOptions) *EventIterator + PollEvents(*EventsOptions) *EventPoller + SetAPIBase(url string) } // MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API. // Colloquially, we refer to instances of this structure as "clients." type MailgunImpl struct { + apiBase string domain string apiKey string publicApiKey string client *http.Client + baseURL string } // NewMailGun creates a new client instance. func NewMailgun(domain, apiKey, publicApiKey string) Mailgun { m := MailgunImpl{ + apiBase: ApiBase, domain: domain, apiKey: apiKey, publicApiKey: publicApiKey, @@ -212,6 +230,42 @@ func NewMailgun(domain, apiKey, publicApiKey string) Mailgun { return &m } +// NewMailgunImpl creates a new client instance. +func NewMailgunImpl(domain, apiKey, publicApiKey string) *MailgunImpl { + return NewMailgun(domain, apiKey, publicApiKey).(*MailgunImpl) +} + +// Return a new Mailgun client using the environment variables +// MG_API_KEY, MG_DOMAIN, MG_PUBLIC_API_KEY and MG_URL +func NewMailgunFromEnv() (Mailgun, error) { + apiKey := os.Getenv("MG_API_KEY") + if apiKey == "" { + return nil, errors.New("required environment variable MG_API_KEY not defined") + } + domain := os.Getenv("MG_DOMAIN") + if domain == "" { + return nil, errors.New("required environment variable MG_DOMAIN not defined") + } + + mg := MailgunImpl{ + apiBase: ApiBase, + domain: domain, + apiKey: apiKey, + publicApiKey: os.Getenv("MG_PUBLIC_API_KEY"), + client: http.DefaultClient, + } + url := os.Getenv("MG_URL") + if url != "" { + mg.SetAPIBase(url) + } + return &mg, nil +} + +// ApiBase returns the API Base URL configured for this client. +func (m *MailgunImpl) ApiBase() string { + return m.apiBase +} + // Domain returns the domain configured for this client. func (m *MailgunImpl) Domain() string { return m.domain @@ -237,15 +291,25 @@ func (m *MailgunImpl) SetClient(c *http.Client) { m.client = c } +// SetAPIBase updates the API Base URL for this client. +func (m *MailgunImpl) SetAPIBase(address string) { + m.apiBase = address +} + // generateApiUrl renders a URL for an API endpoint using the domain and endpoint name. func generateApiUrl(m Mailgun, endpoint string) string { - return fmt.Sprintf("%s/%s/%s", apiBase, m.Domain(), endpoint) + return fmt.Sprintf("%s/%s/%s", m.ApiBase(), m.Domain(), endpoint) +} + +// generateApiUrlWithDomain renders a URL for an API endpoint using a separate domain and endpoint name. +func generateApiUrlWithDomain(m Mailgun, endpoint, domain string) string { + return fmt.Sprintf("%s/%s/%s", m.ApiBase(), domain, endpoint) } // generateMemberApiUrl renders a URL relevant for specifying mailing list members. // The address parameter refers to the mailing list in question. -func generateMemberApiUrl(endpoint, address string) string { - return fmt.Sprintf("%s/%s/%s/members", apiBase, endpoint, address) +func generateMemberApiUrl(m Mailgun, endpoint, address string) string { + return fmt.Sprintf("%s/%s/%s/members", m.ApiBase(), endpoint, address) } // generateApiUrlWithTarget works as generateApiUrl, @@ -263,7 +327,7 @@ func generateApiUrlWithTarget(m Mailgun, endpoint, target string) string { // Most URLs consume a domain in the 2nd position, but some endpoints // require the word "domains" to be there instead. func generateDomainApiUrl(m Mailgun, endpoint string) string { - return fmt.Sprintf("%s/domains/%s/%s", apiBase, m.Domain(), endpoint) + return fmt.Sprintf("%s/domains/%s/%s", m.ApiBase(), m.Domain(), endpoint) } // generateCredentialsUrl renders a URL as generateDomainApiUrl, @@ -284,8 +348,8 @@ func generateStoredMessageUrl(m Mailgun, endpoint, id string) string { } // generatePublicApiUrl works as generateApiUrl, except that generatePublicApiUrl has no need for the domain. -func generatePublicApiUrl(endpoint string) string { - return fmt.Sprintf("%s/%s", apiBase, endpoint) +func generatePublicApiUrl(m Mailgun, endpoint string) string { + return fmt.Sprintf("%s/%s", m.ApiBase(), endpoint) } // generateParameterizedUrl works as generateApiUrl, but supports query parameters. diff --git a/vendor/github.com/mailgun/mailgun-go/mailing_lists.go b/vendor/github.com/mailgun/mailgun-go/mailing_lists.go index fe6afa85e..22ba3564d 100644 --- a/vendor/github.com/mailgun/mailgun-go/mailing_lists.go +++ b/vendor/github.com/mailgun/mailgun-go/mailing_lists.go @@ -43,12 +43,12 @@ var ( // // AccessLevel may be one of ReadOnly, Members, or Everyone. type List struct { - Address string `json:"address",omitempty"` - Name string `json:"name",omitempty"` - Description string `json:"description",omitempty"` - AccessLevel string `json:"access_level",omitempty"` - CreatedAt string `json:"created_at",omitempty"` - MembersCount int `json:"members_count",omitempty"` + Address string `json:"address,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AccessLevel string `json:"access_level,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + MembersCount int `json:"members_count,omitempty"` } // A Member structure represents a member of the mailing list. @@ -62,7 +62,7 @@ type Member struct { // GetLists returns the specified set of mailing lists administered by your account. func (mg *MailgunImpl) GetLists(limit, skip int, filter string) (int, []List, error) { - r := newHTTPRequest(generatePublicApiUrl(listsEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newUrlEncodedPayload() @@ -93,7 +93,7 @@ func (mg *MailgunImpl) GetLists(limit, skip int, filter string) (int, []List, er // If unspecified, Description remains blank, // while AccessLevel defaults to Everyone. func (mg *MailgunImpl) CreateList(prototype List) (List, error) { - r := newHTTPRequest(generatePublicApiUrl(listsEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newUrlEncodedPayload() @@ -121,7 +121,7 @@ func (mg *MailgunImpl) CreateList(prototype List) (List, error) { // DeleteList removes all current members of the list, then removes the list itself. // Attempts to send e-mail to the list will fail subsequent to this call. func (mg *MailgunImpl) DeleteList(addr string) error { - r := newHTTPRequest(generatePublicApiUrl(listsEndpoint) + "/" + addr) + r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/" + addr) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) _, err := makeDeleteRequest(r) @@ -131,10 +131,14 @@ func (mg *MailgunImpl) DeleteList(addr string) error { // GetListByAddress allows your application to recover the complete List structure // representing a mailing list, so long as you have its e-mail address. func (mg *MailgunImpl) GetListByAddress(addr string) (List, error) { - r := newHTTPRequest(generatePublicApiUrl(listsEndpoint) + "/" + addr) + r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/" + addr) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) response, err := makeGetRequest(r) + if err != nil { + return List{}, err + } + var envelope struct { List `json:"list"` } @@ -150,7 +154,7 @@ func (mg *MailgunImpl) GetListByAddress(addr string) (List, error) { // e-mail sent to the old address will not succeed. // Make sure you account for the change accordingly. func (mg *MailgunImpl) UpdateList(addr string, prototype List) (List, error) { - r := newHTTPRequest(generatePublicApiUrl(listsEndpoint) + "/" + addr) + r := newHTTPRequest(generatePublicApiUrl(mg, listsEndpoint) + "/" + addr) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newUrlEncodedPayload() @@ -180,7 +184,7 @@ func (mg *MailgunImpl) UpdateList(addr string, prototype List) (List, error) { // All indicates that you want both Members and unsubscribed members alike, while // Subscribed and Unsubscribed indicate you want only those eponymous subsets. func (mg *MailgunImpl) GetMembers(limit, skip int, s *bool, addr string) (int, []Member, error) { - r := newHTTPRequest(generateMemberApiUrl(listsEndpoint, addr)) + r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newUrlEncodedPayload() @@ -208,7 +212,7 @@ func (mg *MailgunImpl) GetMembers(limit, skip int, s *bool, addr string) (int, [ // GetMemberByAddress returns a complete Member structure for a member of a mailing list, // given only their subscription e-mail address. func (mg *MailgunImpl) GetMemberByAddress(s, l string) (Member, error) { - r := newHTTPRequest(generateMemberApiUrl(listsEndpoint, l) + "/" + s) + r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, l) + "/" + s) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) response, err := makeGetRequest(r) @@ -231,7 +235,7 @@ func (mg *MailgunImpl) CreateMember(merge bool, addr string, prototype Member) e return err } - r := newHTTPRequest(generateMemberApiUrl(listsEndpoint, addr)) + r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newFormDataPayload() @@ -249,7 +253,7 @@ func (mg *MailgunImpl) CreateMember(merge bool, addr string, prototype Member) e // UpdateMember lets you change certain details about the indicated mailing list member. // Address, Name, Vars, and Subscribed fields may be changed. func (mg *MailgunImpl) UpdateMember(s, l string, prototype Member) (Member, error) { - r := newHTTPRequest(generateMemberApiUrl(listsEndpoint, l) + "/" + s) + r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, l) + "/" + s) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newFormDataPayload() @@ -282,7 +286,7 @@ func (mg *MailgunImpl) UpdateMember(s, l string, prototype Member) (Member, erro // DeleteMember removes the member from the list. func (mg *MailgunImpl) DeleteMember(member, addr string) error { - r := newHTTPRequest(generateMemberApiUrl(listsEndpoint, addr) + "/" + member) + r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr) + "/" + member) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) _, err := makeDeleteRequest(r) @@ -299,7 +303,7 @@ func (mg *MailgunImpl) DeleteMember(member, addr string) error { // Otherwise, each Member needs to have at least the Address field filled out. // Other fields are optional, but may be set according to your needs. func (mg *MailgunImpl) CreateMemberList(u *bool, addr string, newMembers []interface{}) error { - r := newHTTPRequest(generateMemberApiUrl(listsEndpoint, addr) + ".json") + r := newHTTPRequest(generateMemberApiUrl(mg, listsEndpoint, addr) + ".json") r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newFormDataPayload() diff --git a/vendor/github.com/mailgun/mailgun-go/messages.go b/vendor/github.com/mailgun/mailgun-go/messages.go index 2264a7099..c7fa3cf3b 100644 --- a/vendor/github.com/mailgun/mailgun-go/messages.go +++ b/vendor/github.com/mailgun/mailgun-go/messages.go @@ -23,6 +23,7 @@ type Message struct { inlines []string readerInlines []ReaderAttachment + nativeSend bool testMode bool tracking bool trackingClicks bool @@ -30,6 +31,7 @@ type Message struct { headers map[string]string variables map[string]string recipientVariables map[string]map[string]interface{} + domain string dkimSet bool trackingSet bool @@ -371,6 +373,12 @@ func (m *Message) SetDKIM(dkim bool) { m.dkimSet = true } +// EnableNativeSend allows the return path to match the address in the Message.Headers.From: +// field when sending from Mailgun rather than the usual bounce+ address in the return path. +func (m *Message) EnableNativeSend() { + m.nativeSend = true +} + // EnableTestMode allows submittal of a message, such that it will be discarded by Mailgun. // This facilitates testing client-side software without actually consuming e-mail resources. func (m *Message) EnableTestMode() { @@ -433,6 +441,11 @@ func (m *Message) AddVariable(variable string, value interface{}) error { return nil } +// AddDomain allows you to use a separate domain for the type of messages you are sending. +func (m *Message) AddDomain(domain string) { + m.domain = domain +} + // Send attempts to queue a message (see Message, NewMessage, and its methods) for delivery. // It returns the Mailgun server response, which consists of two components: // a human-readable status message, and a message ID. The status and message ID are set only @@ -440,86 +453,93 @@ func (m *Message) AddVariable(variable string, value interface{}) error { func (m *MailgunImpl) Send(message *Message) (mes string, id string, err error) { if !isValid(message) { err = errors.New("Message not valid") - } else { - payload := newFormDataPayload() + return + } + payload := newFormDataPayload() - message.specific.addValues(payload) - for _, to := range message.to { - payload.addValue("to", to) - } - for _, tag := range message.tags { - payload.addValue("o:tag", tag) - } - for _, campaign := range message.campaigns { - payload.addValue("o:campaign", campaign) - } - if message.dkimSet { - payload.addValue("o:dkim", yesNo(message.dkim)) - } - if message.deliveryTime != nil { - payload.addValue("o:deliverytime", formatMailgunTime(message.deliveryTime)) - } - if message.testMode { - payload.addValue("o:testmode", "yes") - } - if message.trackingSet { - payload.addValue("o:tracking", yesNo(message.tracking)) - } - if message.trackingClicksSet { - payload.addValue("o:tracking-clicks", yesNo(message.trackingClicks)) - } - if message.trackingOpensSet { - payload.addValue("o:tracking-opens", yesNo(message.trackingOpens)) - } - if message.headers != nil { - for header, value := range message.headers { - payload.addValue("h:"+header, value) - } + message.specific.addValues(payload) + for _, to := range message.to { + payload.addValue("to", to) + } + for _, tag := range message.tags { + payload.addValue("o:tag", tag) + } + for _, campaign := range message.campaigns { + payload.addValue("o:campaign", campaign) + } + if message.dkimSet { + payload.addValue("o:dkim", yesNo(message.dkim)) + } + if message.deliveryTime != nil { + payload.addValue("o:deliverytime", formatMailgunTime(message.deliveryTime)) + } + if message.nativeSend { + payload.addValue("o:native-send", "yes") + } + if message.testMode { + payload.addValue("o:testmode", "yes") + } + if message.trackingSet { + payload.addValue("o:tracking", yesNo(message.tracking)) + } + if message.trackingClicksSet { + payload.addValue("o:tracking-clicks", yesNo(message.trackingClicks)) + } + if message.trackingOpensSet { + payload.addValue("o:tracking-opens", yesNo(message.trackingOpens)) + } + if message.headers != nil { + for header, value := range message.headers { + payload.addValue("h:"+header, value) } - if message.variables != nil { - for variable, value := range message.variables { - payload.addValue("v:"+variable, value) - } + } + if message.variables != nil { + for variable, value := range message.variables { + payload.addValue("v:"+variable, value) } - if message.recipientVariables != nil { - j, err := json.Marshal(message.recipientVariables) - if err != nil { - return "", "", err - } - payload.addValue("recipient-variables", string(j)) + } + if message.recipientVariables != nil { + j, err := json.Marshal(message.recipientVariables) + if err != nil { + return "", "", err } - if message.attachments != nil { - for _, attachment := range message.attachments { - payload.addFile("attachment", attachment) - } + payload.addValue("recipient-variables", string(j)) + } + if message.attachments != nil { + for _, attachment := range message.attachments { + payload.addFile("attachment", attachment) } - if message.readerAttachments != nil { - for _, readerAttachment := range message.readerAttachments { - payload.addReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser) - } + } + if message.readerAttachments != nil { + for _, readerAttachment := range message.readerAttachments { + payload.addReadCloser("attachment", readerAttachment.Filename, readerAttachment.ReadCloser) } - if message.inlines != nil { - for _, inline := range message.inlines { - payload.addFile("inline", inline) - } + } + if message.inlines != nil { + for _, inline := range message.inlines { + payload.addFile("inline", inline) } + } - if message.readerInlines != nil { - for _, readerAttachment := range message.readerInlines { - payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser) - } + if message.readerInlines != nil { + for _, readerAttachment := range message.readerInlines { + payload.addReadCloser("inline", readerAttachment.Filename, readerAttachment.ReadCloser) } + } + + if message.domain == "" { + message.domain = m.Domain() + } - r := newHTTPRequest(generateApiUrl(m, message.specific.endpoint())) - r.setClient(m.Client()) - r.setBasicAuth(basicAuthUser, m.ApiKey()) + r := newHTTPRequest(generateApiUrlWithDomain(m, message.specific.endpoint(), message.domain)) + r.setClient(m.Client()) + r.setBasicAuth(basicAuthUser, m.ApiKey()) - var response sendMessageResponse - err = postResponseFromJSON(r, payload, &response) - if err == nil { - mes = response.Message - id = response.Id - } + var response sendMessageResponse + err = postResponseFromJSON(r, payload, &response) + if err == nil { + mes = response.Message + id = response.Id } return @@ -656,6 +676,32 @@ func (mg *MailgunImpl) GetStoredMessageRaw(id string) (StoredMessageRaw, error) r.setBasicAuth(basicAuthUser, mg.ApiKey()) r.addHeader("Accept", "message/rfc2822") + var response StoredMessageRaw + err := getResponseFromJSON(r, &response) + return response, err +} + +// GetStoredMessageForURL retrieves information about a received e-mail message. +// This provides visibility into, e.g., replies to a message sent to a mailing list. +func (mg *MailgunImpl) GetStoredMessageForURL(url string) (StoredMessage, error) { + r := newHTTPRequest(url) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.ApiKey()) + + var response StoredMessage + err := getResponseFromJSON(r, &response) + return response, err +} + +// GetStoredMessageRawForURL retrieves the raw MIME body of a received e-mail message. +// Compared to GetStoredMessage, it gives access to the unparsed MIME body, and +// thus delegates to the caller the required parsing. +func (mg *MailgunImpl) GetStoredMessageRawForURL(url string) (StoredMessageRaw, error) { + r := newHTTPRequest(url) + r.setClient(mg.Client()) + r.setBasicAuth(basicAuthUser, mg.ApiKey()) + r.addHeader("Accept", "message/rfc2822") + var response StoredMessageRaw err := getResponseFromJSON(r, &response) return response, err diff --git a/vendor/github.com/mailgun/mailgun-go/recipients.go b/vendor/github.com/mailgun/mailgun-go/recipients.go new file mode 100644 index 000000000..317884931 --- /dev/null +++ b/vendor/github.com/mailgun/mailgun-go/recipients.go @@ -0,0 +1,42 @@ +package mailgun + +import "fmt" +import "strings" + +type Recipient struct { + Name string `json:"-"` + Email string `json:"-"` +} + +func (r Recipient) String() string { + if r.Name != "" { + return fmt.Sprintf("%s <%s>", r.Name, r.Email) + } + return r.Email +} + +// MarshalText satisfies TextMarshaler +func (r Recipient) MarshalText() ([]byte, error) { + return []byte(r.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (r *Recipient) UnmarshalText(text []byte) error { + s := string(text) + if s[len(s)-1:] != ">" { + *r = Recipient{Email: s} + return nil + } + + i := strings.Index(s, "<") + // at least 1 char followed by a space + if i < 2 { + return fmt.Errorf("malformed recipient string '%s'", s) + } + *r = Recipient{ + Name: strings.TrimSpace(s[:i]), + Email: s[i+1 : len(s)-1], + } + + return nil +} diff --git a/vendor/github.com/mailgun/mailgun-go/rest_shim.go b/vendor/github.com/mailgun/mailgun-go/rest_shim.go index e575210d6..59984957e 100644 --- a/vendor/github.com/mailgun/mailgun-go/rest_shim.go +++ b/vendor/github.com/mailgun/mailgun-go/rest_shim.go @@ -151,3 +151,12 @@ func makeDeleteRequest(r *httpRequest) (*httpResponse, error) { } return rsp, err } + +// Extract the http status code from error object +func GetStatusFromErr(err error) int { + obj, ok := err.(*UnexpectedResponseError) + if !ok { + return -1 + } + return obj.Actual +} diff --git a/vendor/github.com/mailgun/mailgun-go/routes.go b/vendor/github.com/mailgun/mailgun-go/routes.go index d1460953d..aae319d92 100644 --- a/vendor/github.com/mailgun/mailgun-go/routes.go +++ b/vendor/github.com/mailgun/mailgun-go/routes.go @@ -1,6 +1,7 @@ package mailgun import ( + "fmt" "strconv" ) @@ -33,7 +34,7 @@ type Route struct { // messages sent to a specfic address on your domain. // See the Mailgun documentation for more information. func (mg *MailgunImpl) GetRoutes(limit, skip int) (int, []Route, error) { - r := newHTTPRequest(generatePublicApiUrl(routesEndpoint)) + r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint)) if limit != DefaultLimit { r.addParameter("limit", strconv.Itoa(limit)) } @@ -42,13 +43,13 @@ func (mg *MailgunImpl) GetRoutes(limit, skip int) (int, []Route, error) { } r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) - var envelope struct { TotalCount int `json:"total_count"` Items []Route `json:"items"` } err := getResponseFromJSON(r, &envelope) if err != nil { + fmt.Printf("err here\n") return -1, nil, err } return envelope.TotalCount, envelope.Items, nil @@ -58,8 +59,8 @@ func (mg *MailgunImpl) GetRoutes(limit, skip int) (int, []Route, error) { // The route structure you provide serves as a template, and // only a subset of the fields influence the operation. // See the Route structure definition for more details. -func (mg *MailgunImpl) CreateRoute(prototype Route) (Route, error) { - r := newHTTPRequest(generatePublicApiUrl(routesEndpoint)) +func (mg *MailgunImpl) CreateRoute(prototype Route) (_ignored Route, err error) { + r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint)) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newUrlEncodedPayload() @@ -73,7 +74,9 @@ func (mg *MailgunImpl) CreateRoute(prototype Route) (Route, error) { Message string `json:"message"` *Route `json:"route"` } - err := postResponseFromJSON(r, p, &envelope) + if err = postResponseFromJSON(r, p, &envelope); err != nil { + return _ignored, err + } return *envelope.Route, err } @@ -81,7 +84,7 @@ func (mg *MailgunImpl) CreateRoute(prototype Route) (Route, error) { // To avoid ambiguity, Mailgun identifies the route by unique ID. // See the Route structure definition and the Mailgun API documentation for more details. func (mg *MailgunImpl) DeleteRoute(id string) error { - r := newHTTPRequest(generatePublicApiUrl(routesEndpoint) + "/" + id) + r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint) + "/" + id) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) _, err := makeDeleteRequest(r) @@ -90,7 +93,7 @@ func (mg *MailgunImpl) DeleteRoute(id string) error { // GetRouteByID retrieves the complete route definition associated with the unique route ID. func (mg *MailgunImpl) GetRouteByID(id string) (Route, error) { - r := newHTTPRequest(generatePublicApiUrl(routesEndpoint) + "/" + id) + r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint) + "/" + id) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) var envelope struct { @@ -98,14 +101,18 @@ func (mg *MailgunImpl) GetRouteByID(id string) (Route, error) { *Route `json:"route"` } err := getResponseFromJSON(r, &envelope) + if err != nil { + return Route{}, err + } return *envelope.Route, err + } // UpdateRoute provides an "in-place" update of the specified route. // Only those route fields which are non-zero or non-empty are updated. // All other fields remain as-is. func (mg *MailgunImpl) UpdateRoute(id string, route Route) (Route, error) { - r := newHTTPRequest(generatePublicApiUrl(routesEndpoint) + "/" + id) + r := newHTTPRequest(generatePublicApiUrl(mg, routesEndpoint) + "/" + id) r.setClient(mg.Client()) r.setBasicAuth(basicAuthUser, mg.ApiKey()) p := newUrlEncodedPayload() diff --git a/vendor/github.com/mailgun/mailgun-go/stats.go b/vendor/github.com/mailgun/mailgun-go/stats.go index 4313eb93f..cfeaf81e1 100644 --- a/vendor/github.com/mailgun/mailgun-go/stats.go +++ b/vendor/github.com/mailgun/mailgun-go/stats.go @@ -20,6 +20,59 @@ type statsEnvelope struct { Items []Stat `json:"items"` } +type Accepted struct { + Incoming int `json:"incoming"` + Outgoing int `json:"outgoing"` + Total int `json:"total"` +} + +type Delivered struct { + Smtp int `json:"smtp"` + Http int `json:"http"` + Total int `json:"total"` +} + +type Temporary struct { + Espblock int `json:"espblock"` +} + +type Permanent struct { + SuppressBounce int `json:"suppress-bounce"` + SuppressUnsubscribe int `json:"suppress-unsubscribe"` + SuppressComplaint int `json:"suppress-complaint"` + Bounce int `json:"bounce"` + DelayedBounce int `json:"delayed-bounce"` + Total int `json:"total"` +} + +type Failed struct { + Temporary Temporary `json:"temporary"` + Permanent Permanent `json:"permanent"` +} + +type Total struct { + Total int `json:"total"` +} + +type StatsTotal struct { + Time string `json:"time"` + Accepted Accepted `json:"accepted"` + Delivered Delivered `json:"delivered"` + Failed Failed `json:"failed"` + Stored Total `json:"stored"` + Opened Total `json:"opened"` + Clicked Total `json:"clicked"` + Unsubscribed Total `json:"unsubscribed"` + Complained Total `json:"complained"` +} + +type StatsTotalResponse struct { + End string `json:"end"` + Resolution string `json:"resolution"` + Start string `json:"start"` + Stats []StatsTotal `json:"stats"` +} + // GetStats returns a basic set of statistics for different events. // Events start at the given start date, if one is provided. // If not, this function will consider all stated events dating to the creation of the sending domain. @@ -52,11 +105,37 @@ func (m *MailgunImpl) GetStats(limit int, skip int, startDate *time.Time, event } } -// DeleteTag removes all counters for a particular tag, including the tag itself. -func (m *MailgunImpl) DeleteTag(tag string) error { - r := newHTTPRequest(generateApiUrl(m, deleteTagEndpoint) + "/" + tag) +// GetStatsTotal returns a basic set of statistics for different events. +// https://documentation.mailgun.com/en/latest/api-stats.html#stats +func (m *MailgunImpl) GetStatsTotal(start *time.Time, end *time.Time, resolution string, duration string, event ...string) (*StatsTotalResponse, error) { + r := newHTTPRequest(generateApiUrl(m, statsTotalEndpoint)) + + if start != nil { + r.addParameter("start", start.Format(iso8601date)) + } + if end != nil { + r.addParameter("end", end.Format(iso8601date)) + } + + if resolution != "" { + r.addParameter("resolution", resolution) + } + + if duration != "" { + r.addParameter("duration", duration) + } + + for _, e := range event { + r.addParameter("event", e) + } r.setClient(m.Client()) r.setBasicAuth(basicAuthUser, m.ApiKey()) - _, err := makeDeleteRequest(r) - return err + + var res StatsTotalResponse + err := getResponseFromJSON(r, &res) + if err != nil { + return nil, err + } else { + return &res, nil + } } diff --git a/vendor/github.com/mailgun/mailgun-go/tags.go b/vendor/github.com/mailgun/mailgun-go/tags.go new file mode 100644 index 000000000..0d4ee151f --- /dev/null +++ b/vendor/github.com/mailgun/mailgun-go/tags.go @@ -0,0 +1,177 @@ +package mailgun + +import ( + "net/url" + "strconv" + "time" +) + +type TagItem struct { + Value string `json:"tag"` + Description string `json:"description"` + FirstSeen *time.Time `json:"first-seen,omitempty"` + LastSeen *time.Time `json:"last-seen,omitempty"` +} + +type TagsPage struct { + Items []TagItem `json:"items"` + Paging Paging `json:"paging"` +} + +type TagOptions struct { + // Restrict the page size to this limit + Limit int + // Return only the tags starting with the given prefix + Prefix string + // The page direction based off the 'tag' parameter; valid choices are (first, last, next, prev) + Page string + // The tag that marks piviot point for the 'page' parameter + Tag string +} + +// DeleteTag removes all counters for a particular tag, including the tag itself. +func (m *MailgunImpl) DeleteTag(tag string) error { + r := newHTTPRequest(generateApiUrl(m, tagsEndpoint) + "/" + tag) + r.setClient(m.Client()) + r.setBasicAuth(basicAuthUser, m.ApiKey()) + _, err := makeDeleteRequest(r) + return err +} + +// GetTag retrieves metadata about the tag from the api +func (m *MailgunImpl) GetTag(tag string) (TagItem, error) { + r := newHTTPRequest(generateApiUrl(m, tagsEndpoint) + "/" + tag) + r.setClient(m.Client()) + r.setBasicAuth(basicAuthUser, m.ApiKey()) + var tagItem TagItem + return tagItem, getResponseFromJSON(r, &tagItem) +} + +// ListTags returns a cursor used to iterate through a list of tags +// it := mg.ListTags(nil) +// var page TagsPage +// for it.Next(&page) { +// for _, tag := range(page.Items) { +// // Do stuff with tags +// } +// } +// if it.Err() != nil { +// log.Fatal(it.Err()) +// } +func (m *MailgunImpl) ListTags(opts *TagOptions) *TagIterator { + req := newHTTPRequest(generateApiUrl(m, tagsEndpoint)) + if opts != nil { + if opts.Limit != 0 { + req.addParameter("limit", strconv.Itoa(opts.Limit)) + } + if opts.Prefix != "" { + req.addParameter("prefix", opts.Prefix) + } + if opts.Page != "" { + req.addParameter("page", opts.Page) + } + if opts.Tag != "" { + req.addParameter("tag", opts.Tag) + } + } + + initialUrl, _ := req.generateUrlWithParameters() + tagPage := TagsPage{ + Paging: Paging{ + First: initialUrl, + Next: initialUrl, + Last: initialUrl, + Previous: initialUrl, + }, + } + return NewTagCursor(tagPage, m) +} + +type TagIterator struct { + mg Mailgun + curr TagsPage + err error +} + +// Creates a new cursor from a taglist +func NewTagCursor(tagPage TagsPage, mailgun Mailgun) *TagIterator { + return &TagIterator{curr: tagPage, mg: mailgun} +} + +// Returns the next page in the list of tags +func (t *TagIterator) Next(tagPage *TagsPage) bool { + if !canFetchPage(t.curr.Paging.Next) { + return false + } + + if err := t.cursorRequest(tagPage, t.curr.Paging.Next); err != nil { + t.err = err + return false + } + t.curr = *tagPage + return true +} + +// Returns the previous page in the list of tags +func (t *TagIterator) Previous(tagPage *TagsPage) bool { + if !canFetchPage(t.curr.Paging.Previous) { + return false + } + + if err := t.cursorRequest(tagPage, t.curr.Paging.Previous); err != nil { + t.err = err + return false + } + t.curr = *tagPage + return true +} + +// Returns the first page in the list of tags +func (t *TagIterator) First(tagPage *TagsPage) bool { + if err := t.cursorRequest(tagPage, t.curr.Paging.First); err != nil { + t.err = err + return false + } + t.curr = *tagPage + return true +} + +// Returns the last page in the list of tags +func (t *TagIterator) Last(tagPage *TagsPage) bool { + if err := t.cursorRequest(tagPage, t.curr.Paging.Last); err != nil { + t.err = err + return false + } + t.curr = *tagPage + return true +} + +// Return any error if one occurred +func (t *TagIterator) Err() error { + return t.err +} + +func (t *TagIterator) cursorRequest(tagPage *TagsPage, url string) error { + req := newHTTPRequest(url) + req.setClient(t.mg.Client()) + req.setBasicAuth(basicAuthUser, t.mg.ApiKey()) + return getResponseFromJSON(req, tagPage) +} + +func canFetchPage(slug string) bool { + parts, err := url.Parse(slug) + if err != nil { + return false + } + params, _ := url.ParseQuery(parts.RawQuery) + if err != nil { + return false + } + value, ok := params["tag"] + // If tags doesn't exist, it's our first time fetching pages + if !ok { + return true + } + // If tags has no value, there are no more pages to fetch + return len(value) == 0 +} diff --git a/vendor/github.com/mailgun/mailgun-go/unsubscribes.go b/vendor/github.com/mailgun/mailgun-go/unsubscribes.go index 59a996a3b..bcf82066d 100644 --- a/vendor/github.com/mailgun/mailgun-go/unsubscribes.go +++ b/vendor/github.com/mailgun/mailgun-go/unsubscribes.go @@ -5,10 +5,10 @@ import ( ) type Unsubscription struct { - CreatedAt string `json:"created_at"` - Tag string `json:"tag"` - ID string `json:"id"` - Address string `json:"address"` + CreatedAt string `json:"created_at"` + Tags []string `json:"tags"` + ID string `json:"id"` + Address string `json:"address"` } // GetUnsubscribes retrieves a list of unsubscriptions issued by recipients of mail from your domain. diff --git a/vendor/github.com/mailgun/mailgun-go/webhooks.go b/vendor/github.com/mailgun/mailgun-go/webhooks.go index b56ff3b83..eb2ed9fea 100644 --- a/vendor/github.com/mailgun/mailgun-go/webhooks.go +++ b/vendor/github.com/mailgun/mailgun-go/webhooks.go @@ -79,11 +79,11 @@ func (mg *MailgunImpl) UpdateWebhook(t, u string) error { func (mg *MailgunImpl) VerifyWebhookRequest(req *http.Request) (verified bool, err error) { h := hmac.New(sha256.New, []byte(mg.ApiKey())) - io.WriteString(h, req.Form.Get("timestamp")) - io.WriteString(h, req.Form.Get("token")) + io.WriteString(h, req.FormValue("timestamp")) + io.WriteString(h, req.FormValue("token")) calculatedSignature := h.Sum(nil) - signature, err := hex.DecodeString(req.Form.Get("signature")) + signature, err := hex.DecodeString(req.FormValue("signature")) if err != nil { return false, err } diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go index 2164497ad..82568a1bb 100644 --- a/vendor/github.com/mattn/go-runewidth/runewidth.go +++ b/vendor/github.com/mattn/go-runewidth/runewidth.go @@ -1,13 +1,24 @@ package runewidth +import "os" + var ( // EastAsianWidth will be set true if the current locale is CJK - EastAsianWidth = IsEastAsian() + EastAsianWidth bool // DefaultCondition is a condition in current locale DefaultCondition = &Condition{EastAsianWidth} ) +func init() { + env := os.Getenv("RUNEWIDTH_EASTASIAN") + if env == "" { + EastAsianWidth = IsEastAsian() + } else { + EastAsianWidth = env == "1" + } +} + type interval struct { first rune last rune @@ -55,6 +66,7 @@ var private = table{ var nonprint = table{ {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, + {0x2028, 0x2029}, {0x202A, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, } diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go index 4a7e0b118..a2aa7e5cb 100644 --- a/vendor/github.com/nlopes/slack/admin.go +++ b/vendor/github.com/nlopes/slack/admin.go @@ -62,6 +62,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi "last_name": {lastName}, "ultra_restricted": {"1"}, "token": {api.token}, + "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } @@ -88,6 +89,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe "last_name": {lastName}, "restricted": {"1"}, "token": {api.token}, + "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go index d2b8b2354..326fc010c 100644 --- a/vendor/github.com/nlopes/slack/attachments.go +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -78,6 +78,7 @@ type Attachment struct { CallbackID string `json:"callback_id,omitempty"` ID int `json:"id,omitempty"` + AuthorID string `json:"author_id,omitempty"` AuthorName string `json:"author_name,omitempty"` AuthorSubname string `json:"author_subname,omitempty"` AuthorLink string `json:"author_link,omitempty"` diff --git a/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go index e555a1ad4..197bce2e5 100644 --- a/vendor/github.com/nlopes/slack/backoff.go +++ b/vendor/github.com/nlopes/slack/backoff.go @@ -38,7 +38,7 @@ func (b *backoff) Duration() time.Duration { } //calculate this duration dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) - if b.Jitter == true { + if b.Jitter { dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) } //cap! diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go index b16e19ff0..6204315a9 100644 --- a/vendor/github.com/nlopes/slack/channels.go +++ b/vendor/github.com/nlopes/slack/channels.go @@ -52,11 +52,8 @@ func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) "channel": {channelID}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.archive", values, api.debug) + return err } // UnarchiveChannel unarchives the given channel @@ -73,11 +70,8 @@ func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string "channel": {channelID}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.unarchive", values, api.debug) + return err } // CreateChannel creates a channel with the given name and returns a *Channel @@ -247,11 +241,8 @@ func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, us "user": {user}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.kick", values, api.debug) + return err } // GetChannels retrieves all the channels @@ -297,11 +288,8 @@ func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts "ts": {ts}, } - if _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug); err != nil { - return err - } - - return nil + _, err = channelRequest(ctx, api.httpclient, "channels.mark", values, api.debug) + return err } // RenameChannel renames a given channel diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go index fae416b4f..73cde4bb2 100644 --- a/vendor/github.com/nlopes/slack/chat.go +++ b/vendor/github.com/nlopes/slack/chat.go @@ -3,7 +3,6 @@ package slack import ( "context" "encoding/json" - "errors" "net/url" "strings" ) @@ -32,7 +31,7 @@ type chatResponseFull struct { // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request type PostMessageParameters struct { - Username string `json:"user_name"` + Username string `json:"username"` AsUser bool `json:"as_user"` Parse string `json:"parse"` ThreadTimestamp string `json:"thread_ts"` @@ -112,11 +111,10 @@ func (api *Client) PostMessageContext(ctx context.Context, channel, text string, // PostEphemeral sends an ephemeral message to a user in a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. -func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) (string, error) { - options = append(options, MsgOptionPostEphemeral()) +func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { return api.PostEphemeralContext( context.Background(), - channel, + channelID, userID, options..., ) @@ -124,30 +122,19 @@ func (api *Client) PostEphemeral(channel, userID string, options ...MsgOption) ( // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context // For more details, see PostEphemeral documentation -func (api *Client) PostEphemeralContext(ctx context.Context, channel, userID string, options ...MsgOption) (string, error) { - path, values, err := ApplyMsgOptions(api.token, channel, options...) - if err != nil { - return "", err - } - - values.Add("user", userID) - - response, err := chatRequest(ctx, api.httpclient, path, values, api.debug) - if err != nil { - return "", err - } - - return response.Timestamp, nil +func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) { + _, timestamp, _, err = api.SendMessageContext(ctx, channelID, append(options, MsgOptionPostEphemeral2(userID))...) + return timestamp, err } // UpdateMessage updates a message in a channel -func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { - return api.UpdateMessageContext(context.Background(), channel, timestamp, text) +func (api *Client) UpdateMessage(channelID, timestamp, text string) (string, string, string, error) { + return api.UpdateMessageContext(context.Background(), channelID, timestamp, text) } // UpdateMessageContext updates a message in a channel -func (api *Client) UpdateMessageContext(ctx context.Context, channel, timestamp, text string) (string, string, string, error) { - return api.SendMessageContext(ctx, channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) +func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp, text string) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionText(text, true)) } // SendMessage more flexible method for configuring messages. @@ -156,22 +143,30 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st } // SendMessageContext more flexible method for configuring messages with a custom context. -func (api *Client) SendMessageContext(ctx context.Context, channel string, options ...MsgOption) (string, string, string, error) { - channel, values, err := ApplyMsgOptions(api.token, channel, options...) - if err != nil { +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (channel string, timestamp string, text string, err error) { + var ( + config sendConfig + response chatResponseFull + ) + + if config, err = applyMsgOptions(api.token, channelID, options...); err != nil { return "", "", "", err } - response, err := chatRequest(ctx, api.httpclient, channel, values, api.debug) - if err != nil { + if err = post(ctx, api.httpclient, string(config.mode), config.values, &response, api.debug); err != nil { return "", "", "", err } - return response.Channel, response.Timestamp, response.Text, nil + return response.Channel, response.Timestamp, response.Text, response.Err() } // ApplyMsgOptions utility function for debugging/testing chat requests. func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) { + config, err := applyMsgOptions(token, channel, options...) + return string(config.mode), config.values, err +} + +func applyMsgOptions(token, channel string, options ...MsgOption) (sendConfig, error) { config := sendConfig{ mode: chatPostMessage, values: url.Values{ @@ -182,11 +177,11 @@ func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.V for _, opt := range options { if err := opt(&config); err != nil { - return string(config.mode), config.values, err + return config, err } } - return string(config.mode), config.values, nil + return config, nil } func escapeMessage(message string) string { @@ -194,18 +189,6 @@ func escapeMessage(message string) string { return replacer.Replace(message) } -func chatRequest(ctx context.Context, client HTTPRequester, path string, values url.Values, debug bool) (*chatResponseFull, error) { - response := &chatResponseFull{} - err := post(ctx, client, path, values, response, debug) - if err != nil { - return nil, err - } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response, nil -} - type sendMode string const ( @@ -232,7 +215,8 @@ func MsgOptionPost() MsgOption { } } -// MsgOptionPostEphemeral posts an ephemeral message +// MsgOptionPostEphemeral - DEPRECATED: use MsgOptionPostEphemeral2 +// posts an ephemeral message. func MsgOptionPostEphemeral() MsgOption { return func(config *sendConfig) error { config.mode = chatPostEphemeral @@ -241,6 +225,17 @@ func MsgOptionPostEphemeral() MsgOption { } } +// MsgOptionPostEphemeral2 - posts an ephemeral message to the provided user. +func MsgOptionPostEphemeral2(userID string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatPostEphemeral + MsgOptionUser(userID)(config) + config.values.Del("ts") + + return nil + } +} + // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { @@ -269,6 +264,14 @@ func MsgOptionAsUser(b bool) MsgOption { } } +// MsgOptionUser set the user for the message. +func MsgOptionUser(userID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("user", userID) + return nil + } +} + // MsgOptionText provide the text for the message, optionally escape the provided // text. func MsgOptionText(text string, escape bool) MsgOption { @@ -328,11 +331,32 @@ func MsgOptionDisableMarkdown() MsgOption { } } +// this function combines multiple options into a single option. +func MsgOptionCompose(options ...MsgOption) MsgOption { + return func(c *sendConfig) error { + for _, opt := range options { + if err := opt(c); err != nil { + return err + } + } + return nil + } +} + +func MsgOptionParse(b bool) MsgOption { + return func(c *sendConfig) error { + var v string + if b { v = "1" } else { v = "0" } + c.values.Set("parse", v) + return nil + } +} + // MsgOptionPostMessageParameters maintain backwards compatibility. func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return func(config *sendConfig) error { if params.Username != DEFAULT_MESSAGE_USERNAME { - config.values.Set("username", string(params.Username)) + config.values.Set("username", params.Username) } // chat.postEphemeral support @@ -344,7 +368,7 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { MsgOptionAsUser(params.AsUser)(config) if params.Parse != DEFAULT_MESSAGE_PARSE { - config.values.Set("parse", string(params.Parse)) + config.values.Set("parse", params.Parse) } if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { config.values.Set("link_names", "1") diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go index 26ee29277..b2dcc1dc3 100644 --- a/vendor/github.com/nlopes/slack/conversation.go +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -83,7 +83,7 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge values.Add("cursor", params.Cursor) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } response := struct { Members []string `json:"members"` @@ -116,10 +116,8 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // UnArchiveConversation reverses conversation archival @@ -138,10 +136,8 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // SetTopicOfConversation sets the topic for a conversation @@ -164,10 +160,8 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // SetPurposeOfConversation sets the purpose for a conversation @@ -190,10 +184,8 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // RenameConversation renames a conversation @@ -216,10 +208,8 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // InviteUsersToConversation invites users to a channel @@ -242,10 +232,8 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return response.Channel, nil + + return response.Channel, response.Err() } // KickUserFromConversation removes a user from a conversation @@ -265,10 +253,8 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // CloseConversation closes a direct message or multi-person direct message @@ -292,10 +278,8 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin if err != nil { return false, false, err } - if !response.Ok { - return false, false, errors.New(response.Error) - } - return response.NoOp, response.AlreadyClosed, nil + + return response.NoOp, response.AlreadyClosed, response.Err() } // CreateConversation initiates a public or private channel-based conversation @@ -315,10 +299,8 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response.Channel, nil + + return &response.Channel, response.Err() } // GetConversationInfo retrieves information about a conversation @@ -338,10 +320,8 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str if err != nil { return nil, err } - if !response.Ok { - return nil, errors.New(response.Error) - } - return &response.Channel, nil + + return &response.Channel, response.Err() } // LeaveConversation leaves a conversation @@ -357,11 +337,7 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin } response, err := channelRequest(ctx, api.httpclient, "conversations.leave", values, api.debug) - if err != nil { - return false, err - } - - return response.NotInChannel, nil + return response.NotInChannel, err } type GetConversationRepliesParameters struct { @@ -393,7 +369,7 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge values.Add("latest", params.Latest) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != "" { values.Add("oldest", params.Oldest) @@ -416,10 +392,8 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge if err != nil { return nil, false, "", err } - if !response.Ok { - return nil, false, "", errors.New(response.Error) - } - return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, nil + + return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err() } type GetConversationsParameters struct { @@ -444,7 +418,7 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve values.Add("cursor", params.Cursor) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Types != nil { values.Add("types", strings.Join(params.Types, ",")) @@ -458,10 +432,8 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve if err != nil { return nil, "", err } - if !response.Ok { - return nil, "", errors.New(response.Error) - } - return response.Channels, response.ResponseMetaData.NextCursor, nil + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() } type OpenConversationParameters struct { @@ -497,10 +469,8 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv if err != nil { return nil, false, false, err } - if !response.Ok { - return nil, false, false, errors.New(response.Error) - } - return response.Channel, response.NoOp, response.AlreadyOpen, nil + + return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() } // JoinConversation joins an existing conversation @@ -523,8 +493,8 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string if err != nil { return nil, "", nil, err } - if !response.Ok { - return nil, "", nil, errors.New(response.Error) + if response.Err() != nil { + return nil, "", nil, response.Err() } var warnings []string if response.ResponseMetaData != nil { @@ -573,7 +543,7 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge values.Add("latest", params.Latest) } if params.Limit != 0 { - values.Add("limit", string(params.Limit)) + values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != "" { values.Add("oldest", params.Oldest) diff --git a/vendor/github.com/nlopes/slack/dnd.go b/vendor/github.com/nlopes/slack/dnd.go index ad8512bee..a4cfbe65c 100644 --- a/vendor/github.com/nlopes/slack/dnd.go +++ b/vendor/github.com/nlopes/slack/dnd.go @@ -64,10 +64,8 @@ func (api *Client) EndDNDContext(ctx context.Context) error { if err := post(ctx, api.httpclient, "dnd.endDnd", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // EndSnooze ends the current user's snooze mode diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go index 555d3a5b0..2381ec3ce 100644 --- a/vendor/github.com/nlopes/slack/files.go +++ b/vendor/github.com/nlopes/slack/files.go @@ -244,9 +244,9 @@ func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParam values.Add("content", params.Content) err = postForm(ctx, api.httpclient, SLACK_API+"files.upload", values, response, api.debug) } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, api.httpclient, SLACK_API+"files.upload", params.File, "file", values, response, api.debug) + err = postLocalWithMultipartResponse(ctx, api.httpclient, "files.upload", params.File, "file", values, response, api.debug) } else if params.Reader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, SLACK_API+"files.upload", params.Filename, "file", values, params.Reader, response, api.debug) + err = postWithMultipartResponse(ctx, api.httpclient, "files.upload", params.Filename, "file", values, params.Reader, response, api.debug) } if err != nil { return nil, err @@ -273,11 +273,8 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment "file": {fileID}, "id": {commentID}, } - if _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug); err != nil { - return err - } - - return nil + _, err = fileRequest(ctx, api.httpclient, "files.comments.delete", values, api.debug) + return err } // DeleteFile deletes a file @@ -292,11 +289,8 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er "file": {fileID}, } - if _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug); err != nil { - return err - } - - return nil + _, err = fileRequest(ctx, api.httpclient, "files.delete", values, api.debug) + return err } // RevokeFilePublicURL disables public/external sharing for a file diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go index d0e7d9136..67e78e993 100644 --- a/vendor/github.com/nlopes/slack/groups.go +++ b/vendor/github.com/nlopes/slack/groups.go @@ -53,9 +53,6 @@ func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error } _, err := groupRequest(ctx, api.httpclient, "groups.archive", values, api.debug) - if err != nil { - return err - } return err } @@ -72,10 +69,7 @@ func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) erro } _, err := groupRequest(ctx, api.httpclient, "groups.unarchive", values, api.debug) - if err != nil { - return err - } - return nil + return err } // CreateGroup creates a private group @@ -215,11 +209,8 @@ func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err err "channel": {group}, } - if _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug); err != nil { - return err - } - - return nil + _, err = groupRequest(ctx, api.httpclient, "groups.leave", values, api.debug) + return err } // KickUserFromGroup kicks a user from a group @@ -235,11 +226,8 @@ func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user str "user": {user}, } - if _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug); err != nil { - return err - } - - return nil + _, err = groupRequest(ctx, api.httpclient, "groups.kick", values, api.debug) + return err } // GetGroups retrieves all groups @@ -300,11 +288,8 @@ func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string "ts": {ts}, } - if _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug); err != nil { - return err - } - - return nil + _, err = groupRequest(ctx, api.httpclient, "groups.mark", values, api.debug) + return err } // OpenGroup opens a private group diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go index 55b24b70d..ef4701495 100644 --- a/vendor/github.com/nlopes/slack/im.go +++ b/vendor/github.com/nlopes/slack/im.go @@ -87,18 +87,15 @@ func (api *Client) MarkIMChannel(channel, ts string) (err error) { } // MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context -func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) (err error) { +func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) error { values := url.Values{ "token": {api.token}, "channel": {channel}, "ts": {ts}, } - _, err = imRequest(ctx, api.httpclient, "im.mark", values, api.debug) - if err != nil { - return err - } - return + _, err := imRequest(ctx, api.httpclient, "im.mark", values, api.debug) + return err } // GetIMHistory retrieves the direct message channel history diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go index 49db5327d..978acd288 100644 --- a/vendor/github.com/nlopes/slack/info.go +++ b/vendor/github.com/nlopes/slack/info.go @@ -156,7 +156,7 @@ type Info struct { type infoResponseFull struct { Info - WebResponse + SlackResponse } // GetBotByID returns a bot given a bot id diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go index 4d9df61fd..27a5ab32d 100644 --- a/vendor/github.com/nlopes/slack/messages.go +++ b/vendor/github.com/nlopes/slack/messages.go @@ -26,7 +26,7 @@ type Msg struct { Timestamp string `json:"ts,omitempty"` ThreadTimestamp string `json:"thread_ts,omitempty"` IsStarred bool `json:"is_starred,omitempty"` - PinnedTo []string `json:"pinned_to, omitempty"` + PinnedTo []string `json:"pinned_to,omitempty"` Attachments []Attachment `json:"attachments,omitempty"` Edited *Edited `json:"edited,omitempty"` LastRead string `json:"last_read,omitempty"` @@ -89,6 +89,7 @@ type Msg struct { // slash commands and interactive messages ResponseType string `json:"response_type,omitempty"` ReplaceOriginal bool `json:"replace_original,omitempty"` + DeleteOriginal bool `json:"delete_original,omitempty"` } // Icon is used for bot messages diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go index 32f236797..620e813cc 100644 --- a/vendor/github.com/nlopes/slack/misc.go +++ b/vendor/github.com/nlopes/slack/misc.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -18,15 +19,17 @@ import ( "time" ) -type WebResponse struct { - Ok bool `json:"ok"` - Error *WebError `json:"error"` +type SlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` } -type WebError string +func (t SlackResponse) Err() error { + if t.Ok { + return nil + } -func (s WebError) Error() string { - return string(s) + return errors.New(t.Error) } type RateLimitedError struct { @@ -63,7 +66,7 @@ func fileUploadReq(ctx context.Context, path, fieldname, filename string, values return req, nil } -func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error { +func parseResponseBody(body io.ReadCloser, intf interface{}, debug bool) error { response, err := ioutil.ReadAll(body) if err != nil { return err @@ -74,7 +77,7 @@ func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error logger.Printf("parseResponseBody: %s\n", string(response)) } - return json.Unmarshal(response, &intf) + return json.Unmarshal(response, intf) } func postLocalWithMultipartResponse(ctx context.Context, client HTTPRequester, path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error { @@ -116,7 +119,7 @@ func postWithMultipartResponse(ctx context.Context, client HTTPRequester, path, return fmt.Errorf("Slack server error: %s.", resp.Status) } - return parseResponseBody(resp.Body, &intf, debug) + return parseResponseBody(resp.Body, intf, debug) } func postForm(ctx context.Context, client HTTPRequester, endpoint string, values url.Values, intf interface{}, debug bool) error { @@ -148,7 +151,7 @@ func postForm(ctx context.Context, client HTTPRequester, endpoint string, values return fmt.Errorf("Slack server error: %s.", resp.Status) } - return parseResponseBody(resp.Body, &intf, debug) + return parseResponseBody(resp.Body, intf, debug) } func post(ctx context.Context, client HTTPRequester, path string, values url.Values, intf interface{}, debug bool) error { @@ -180,3 +183,9 @@ func okJsonHandler(rw http.ResponseWriter, r *http.Request) { }) rw.Write(response) } + +type errorString string + +func (t errorString) Error() string { + return string(t) +} diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go index 6b39778db..da7fe268f 100644 --- a/vendor/github.com/nlopes/slack/pins.go +++ b/vendor/github.com/nlopes/slack/pins.go @@ -24,23 +24,21 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} if err := post(ctx, api.httpclient, "pins.add", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // RemovePin un-pins an item from a channel @@ -55,23 +53,21 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} if err := post(ctx, api.httpclient, "pins.remove", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // ListPins returns information about the items a user reacted to. diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go index c0556d8f6..f3746a3c0 100644 --- a/vendor/github.com/nlopes/slack/reactions.go +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -142,26 +142,24 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite values.Set("name", name) } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} if err := post(ctx, api.httpclient, "reactions.add", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // RemoveReaction removes a reaction emoji from a message, file or file comment. @@ -178,26 +176,24 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item values.Set("name", name) } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} if err := post(ctx, api.httpclient, "reactions.remove", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // GetReactions returns details about the reactions on an item. @@ -211,16 +207,16 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params "token": {api.token}, } if item.Channel != "" { - values.Set("channel", string(item.Channel)) + values.Set("channel", item.Channel) } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } if params.Full != DEFAULT_REACTIONS_FULL { values.Set("full", strconv.FormatBool(params.Full)) diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go index 7b55c2ab3..8dbdb6ec9 100644 --- a/vendor/github.com/nlopes/slack/rtm.go +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -3,9 +3,11 @@ package slack import ( "context" "encoding/json" - "fmt" "net/url" + "sync" "time" + + "github.com/gorilla/websocket" ) const ( @@ -29,13 +31,11 @@ func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketUR response := &infoResponseFull{} err = post(ctx, api.httpclient, "rtm.start", url.Values{"token": {api.token}}, response, api.debug) if err != nil { - return nil, "", fmt.Errorf("post: %s", err) - } - if !response.Ok { - return nil, "", response.Error + return nil, "", err } + api.Debugln("Using URL:", response.Info.URL) - return &response.Info, response.Info.URL, nil + return &response.Info, response.Info.URL, response.Err() } // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block. @@ -48,7 +48,8 @@ func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) { return api.ConnectRTMContext(ctx) } -// ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block with a custom context. +// ConnectRTMContext calls the "rtm.connect" endpoint and returns the +// provided URL and the compact Info block with a custom context. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { @@ -56,25 +57,35 @@ func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocket err = post(ctx, api.httpclient, "rtm.connect", url.Values{"token": {api.token}}, response, api.debug) if err != nil { api.Debugf("Failed to connect to RTM: %s", err) - return nil, "", fmt.Errorf("post: %s", err) - } - if !response.Ok { - return nil, "", response.Error + return nil, "", err } + api.Debugln("Using URL:", response.Info.URL) - return &response.Info, response.Info.URL, nil + return &response.Info, response.Info.URL, response.Err() } -// NewRTM returns a RTM, which provides a fully managed connection to -// Slack's websocket-based Real-Time Messaging protocol. -func (api *Client) NewRTM() *RTM { - return api.NewRTMWithOptions(nil) +// RTMOption options for the managed RTM. +type RTMOption func(*RTM) + +// RTMOptionUseStart as of 11th July 2017 you should prefer setting this to false, see: +// https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start +func RTMOptionUseStart(b bool) RTMOption { + return func(rtm *RTM) { + rtm.useRTMStart = b + } +} + +// RTMOptionDialer takes a gorilla websocket Dialer and uses it as the +// Dialer when opening the websocket for the RTM connection. +func RTMOptionDialer(d *websocket.Dialer) RTMOption { + return func(rtm *RTM) { + rtm.dialer = d + } } -// NewRTMWithOptions returns a RTM, which provides a fully managed connection to +// NewRTM returns a RTM, which provides a fully managed connection to // Slack's websocket-based Real-Time Messaging protocol. -// This also allows to configure various options available for RTM API. -func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { +func (api *Client) NewRTM(options ...RTMOption) *RTM { result := &RTM{ Client: *api, IncomingEvents: make(chan RTMEvent, 50), @@ -87,13 +98,23 @@ func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { forcePing: make(chan bool), rawEvents: make(chan json.RawMessage), idGen: NewSafeID(1), + mu: &sync.Mutex{}, } - if options != nil { - result.useRTMStart = options.UseRTMStart - } else { - result.useRTMStart = true + for _, opt := range options { + opt(result) } return result } + +// NewRTMWithOptions Deprecated just use NewRTM(RTMOptionsUseStart(true)) +// returns a RTM, which provides a fully managed connection to +// Slack's websocket-based Real-Time Messaging protocol. +// This also allows to configure various options available for RTM API. +func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM { + if options != nil { + return api.NewRTM(RTMOptionUseStart(options.UseRTMStart)) + } + return api.NewRTM() +} diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go index 390dcdb27..0cbce29eb 100644 --- a/vendor/github.com/nlopes/slack/search.go +++ b/vendor/github.com/nlopes/slack/search.go @@ -11,7 +11,7 @@ const ( DEFAULT_SEARCH_SORT = "score" DEFAULT_SEARCH_SORT_DIR = "desc" DEFAULT_SEARCH_HIGHLIGHT = false - DEFAULT_SEARCH_COUNT = 100 + DEFAULT_SEARCH_COUNT = 20 DEFAULT_SEARCH_PAGE = 1 ) @@ -37,17 +37,18 @@ type CtxMessage struct { } type SearchMessage struct { - Type string `json:"type"` - Channel CtxChannel `json:"channel"` - User string `json:"user"` - Username string `json:"username"` - Timestamp string `json:"ts"` - Text string `json:"text"` - Permalink string `json:"permalink"` - Previous CtxMessage `json:"previous"` - Previous2 CtxMessage `json:"previous_2"` - Next CtxMessage `json:"next"` - Next2 CtxMessage `json:"next_2"` + Type string `json:"type"` + Channel CtxChannel `json:"channel"` + User string `json:"user"` + Username string `json:"username"` + Timestamp string `json:"ts"` + Text string `json:"text"` + Permalink string `json:"permalink"` + Attachments []Attachment `json:"attachments"` + Previous CtxMessage `json:"previous"` + Previous2 CtxMessage `json:"previous_2"` + Next CtxMessage `json:"next"` + Next2 CtxMessage `json:"next_2"` } type SearchMessages struct { diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go index ddf42e905..ebc9c2d17 100644 --- a/vendor/github.com/nlopes/slack/slack.go +++ b/vendor/github.com/nlopes/slack/slack.go @@ -35,9 +35,17 @@ func SetHTTPClient(client HTTPRequester) { customHTTPClient = client } -type SlackResponse struct { - Ok bool `json:"ok"` - Error string `json:"error"` +// ResponseMetadata holds pagination metadata +type ResponseMetadata struct { + Cursor string `json:"next_cursor"` +} + +func (t *ResponseMetadata) initialize() *ResponseMetadata { + if t != nil { + return t + } + + return &ResponseMetadata{} } type AuthTestResponse struct { diff --git a/vendor/github.com/nlopes/slack/slash.go b/vendor/github.com/nlopes/slack/slash.go index c21a4786c..f62065a27 100644 --- a/vendor/github.com/nlopes/slack/slash.go +++ b/vendor/github.com/nlopes/slack/slash.go @@ -6,17 +6,19 @@ import ( // SlashCommand contains information about a request of the slash command type SlashCommand struct { - Token string `json:"token"` - TeamID string `json:"team_id"` - TeamDomain string `json:"team_domain"` - ChannelID string `json:"channel_id"` - ChannelName string `json:"channel_name"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - Command string `json:"command"` - Text string `json:"text"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` + Token string `json:"token"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EnterpriseID string `json:"enterprise_id,omitempty"` + EnterpriseName string `json:"enterprise_name,omitempty"` + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + Command string `json:"command"` + Text string `json:"text"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` } // SlashCommandParse will parse the request of the slash command @@ -27,6 +29,8 @@ func SlashCommandParse(r *http.Request) (s SlashCommand, err error) { s.Token = r.PostForm.Get("token") s.TeamID = r.PostForm.Get("team_id") s.TeamDomain = r.PostForm.Get("team_domain") + s.EnterpriseID = r.PostForm.Get("enterprise_id") + s.EnterpriseName = r.PostForm.Get("enterprise_name") s.ChannelID = r.PostForm.Get("channel_id") s.ChannelName = r.PostForm.Get("channel_name") s.UserID = r.PostForm.Get("user_id") diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go index 785dec5ed..1fd6ea137 100644 --- a/vendor/github.com/nlopes/slack/stars.go +++ b/vendor/github.com/nlopes/slack/stars.go @@ -48,23 +48,21 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} if err := post(ctx, api.httpclient, "stars.add", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // RemoveStar removes a starred item from a channel @@ -79,23 +77,21 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I "token": {api.token}, } if item.Timestamp != "" { - values.Set("timestamp", string(item.Timestamp)) + values.Set("timestamp", item.Timestamp) } if item.File != "" { - values.Set("file", string(item.File)) + values.Set("file", item.File) } if item.Comment != "" { - values.Set("file_comment", string(item.Comment)) + values.Set("file_comment", item.Comment) } response := &SlackResponse{} if err := post(ctx, api.httpclient, "stars.remove", values, response, api.debug); err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // ListStars returns information about the stars a user added diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go index 5b3dddccf..131eebaa1 100644 --- a/vendor/github.com/nlopes/slack/users.go +++ b/vendor/github.com/nlopes/slack/users.go @@ -5,37 +5,97 @@ import ( "encoding/json" "errors" "net/url" + "strconv" ) const ( DEFAULT_USER_PHOTO_CROP_X = -1 DEFAULT_USER_PHOTO_CROP_Y = -1 DEFAULT_USER_PHOTO_CROP_W = -1 + errPaginationComplete = errorString("pagination complete") ) // UserProfile contains all the information details of a given user type UserProfile struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - DisplayName string `json:"display_name"` - DisplayNameNormalized string `json:"display_name_normalized"` - Email string `json:"email"` - Skype string `json:"skype"` - Phone string `json:"phone"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - Image192 string `json:"image_192"` - ImageOriginal string `json:"image_original"` - Title string `json:"title"` - BotID string `json:"bot_id,omitempty"` - ApiAppID string `json:"api_app_id,omitempty"` - StatusText string `json:"status_text,omitempty"` - StatusEmoji string `json:"status_emoji,omitempty"` - Team string `json:"team"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Email string `json:"email"` + Skype string `json:"skype"` + Phone string `json:"phone"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + ImageOriginal string `json:"image_original"` + Title string `json:"title"` + BotID string `json:"bot_id,omitempty"` + ApiAppID string `json:"api_app_id,omitempty"` + StatusText string `json:"status_text,omitempty"` + StatusEmoji string `json:"status_emoji,omitempty"` + Team string `json:"team"` + Fields UserProfileCustomFields `json:"fields"` +} + +// UserProfileCustomFields represents user profile's custom fields. +// Slack API's response data type is inconsistent so we use the struct. +// For detail, please see below. +// https://github.com/nlopes/slack/pull/298#discussion_r185159233 +type UserProfileCustomFields struct { + fields map[string]UserProfileCustomField +} + +// UnmarshalJSON is the implementation of the json.Unmarshaler interface. +func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error { + // https://github.com/nlopes/slack/pull/298#discussion_r185159233 + if string(b) == "[]" { + return nil + } + return json.Unmarshal(b, &fields.fields) +} + +// MarshalJSON is the implementation of the json.Marshaler interface. +func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) { + if len(fields.fields) == 0 { + return []byte("[]"), nil + } + return json.Marshal(fields.fields) +} + +// ToMap returns a map of custom fields. +func (fields *UserProfileCustomFields) ToMap() map[string]UserProfileCustomField { + return fields.fields +} + +// Len returns the number of custom fields. +func (fields *UserProfileCustomFields) Len() int { + return len(fields.fields) +} + +// SetMap sets a map of custom fields. +func (fields *UserProfileCustomFields) SetMap(m map[string]UserProfileCustomField) { + fields.fields = m +} + +// FieldsMap returns a map of custom fields. +func (profile *UserProfile) FieldsMap() map[string]UserProfileCustomField { + return profile.Fields.ToMap() +} + +// SetFieldsMap sets a map of custom fields. +func (profile *UserProfile) SetFieldsMap(m map[string]UserProfileCustomField) { + profile.Fields.SetMap(m) +} + +// UserProfileCustomField represents a custom user profile field +type UserProfileCustomField struct { + Value string `json:"value"` + Alt string `json:"alt"` + Label string `json:"label"` } // User contains all the information of a user @@ -108,10 +168,11 @@ type TeamIdentity struct { } type userResponseFull struct { - Members []User `json:"members,omitempty"` // ListUsers - User `json:"user,omitempty"` // GetUserInfo - UserPresence // GetUserPresence + Members []User `json:"members,omitempty"` + User `json:"user,omitempty"` + UserPresence SlackResponse + Metadata ResponseMetadata `json:"response_metadata"` } type UserSetPhotoParams struct { @@ -178,23 +239,109 @@ func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, return &response.User, nil } +// GetUsersOption options for the GetUsers method call. +type GetUsersOption func(*UserPagination) + +// GetUsersOptionLimit limit the number of users returned +func GetUsersOptionLimit(n int) GetUsersOption { + return func(p *UserPagination) { + p.limit = n + } +} + +// GetUsersOptionPresence include user presence +func GetUsersOptionPresence(n bool) GetUsersOption { + return func(p *UserPagination) { + p.presence = n + } +} + +func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) { + up = UserPagination{ + c: c, + limit: 200, // per slack api documentation. + } + + for _, opt := range options { + opt(&up) + } + + return up +} + +// UserPagination allows for paginating over the users +type UserPagination struct { + Users []User + limit int + presence bool + previousResp *ResponseMetadata + c *Client +} + +// Done checks if the pagination has completed +func (UserPagination) Done(err error) bool { + return err == errPaginationComplete +} + +// Failure checks if pagination failed. +func (t UserPagination) Failure(err error) error { + if t.Done(err) { + return nil + } + + return err +} + +func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) { + var ( + resp *userResponseFull + ) + + if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + return t, errPaginationComplete + } + + t.previousResp = t.previousResp.initialize() + + values := url.Values{ + "limit": {strconv.Itoa(t.limit)}, + "presence": {strconv.FormatBool(t.presence)}, + "token": {t.c.token}, + "cursor": {t.previousResp.Cursor}, + } + + if resp, err = userRequest(ctx, t.c.httpclient, "users.list", values, t.c.debug); err != nil { + return t, err + } + + t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata) + t.Users = resp.Members + t.previousResp = &resp.Metadata + + return t, nil +} + +// GetUsersPaginated fetches users in a paginated fashion, see GetUsersContext for usage. +func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination { + return newUserPagination(api, options...) +} + // GetUsers returns the list of users (with their detailed information) func (api *Client) GetUsers() ([]User, error) { return api.GetUsersContext(context.Background()) } // GetUsersContext returns the list of users (with their detailed information) with a custom context -func (api *Client) GetUsersContext(ctx context.Context) ([]User, error) { - values := url.Values{ - "token": {api.token}, - "presence": {"1"}, - } +func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) { + var ( + p UserPagination + ) - response, err := userRequest(ctx, api.httpclient, "users.list", values, api.debug) - if err != nil { - return nil, err + for p = api.GetUsersPaginated(); !p.Done(err); p, err = p.Next(ctx) { + results = append(results, p.Users...) } - return response.Members, nil + + return results, p.Failure(err) } // GetUserByEmail will retrieve the complete user information by email @@ -226,11 +373,8 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { "token": {api.token}, } - if _, err := userRequest(ctx, api.httpclient, "users.setActive", values, api.debug); err != nil { - return err - } - - return nil + _, err = userRequest(ctx, api.httpclient, "users.setActive", values, api.debug) + return err } // SetUserPresence changes the currently authenticated user presence @@ -246,11 +390,7 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) } _, err := userRequest(ctx, api.httpclient, "users.setPresence", values, api.debug) - if err != nil { - return err - } - return nil - + return err } // GetUserIdentity will retrieve user info available per identity scopes @@ -287,23 +427,21 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params "token": {api.token}, } if params.CropX != DEFAULT_USER_PHOTO_CROP_X { - values.Add("crop_x", string(params.CropX)) + values.Add("crop_x", strconv.Itoa(params.CropX)) } if params.CropY != DEFAULT_USER_PHOTO_CROP_Y { - values.Add("crop_y", string(params.CropY)) + values.Add("crop_y", strconv.Itoa(params.CropX)) } if params.CropW != DEFAULT_USER_PHOTO_CROP_W { - values.Add("crop_w", string(params.CropW)) + values.Add("crop_w", strconv.Itoa(params.CropW)) } - err := postLocalWithMultipartResponse(ctx, api.httpclient, SLACK_API+"users.setPhoto", image, "image", values, response, api.debug) + err := postLocalWithMultipartResponse(ctx, api.httpclient, "users.setPhoto", image, "image", values, response, api.debug) if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // DeleteUserPhoto deletes the current authenticated user's profile image @@ -322,10 +460,8 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) error { if err != nil { return err } - if !response.Ok { - return errors.New(response.Error) - } - return nil + + return response.Err() } // SetUserCustomStatus will set a custom status and emoji for the currently @@ -392,3 +528,31 @@ func (api *Client) UnsetUserCustomStatus() error { func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { return api.SetUserCustomStatusContext(ctx, "", "") } + +// GetUserProfile retrieves a user's profile information. +func (api *Client) GetUserProfile(userID string, includeLabels bool) (*UserProfile, error) { + return api.GetUserProfileContext(context.Background(), userID, includeLabels) +} + +type getUserProfileResponse struct { + SlackResponse + Profile *UserProfile `json:"profile"` +} + +// GetUserProfileContext retrieves a user's profile information with a context. +func (api *Client) GetUserProfileContext(ctx context.Context, userID string, includeLabels bool) (*UserProfile, error) { + values := url.Values{"token": {api.token}, "user": {userID}} + if includeLabels { + values.Add("include_labels", "true") + } + resp := &getUserProfileResponse{} + + err := post(ctx, api.httpclient, "users.profile.get", values, &resp, api.debug) + if err != nil { + return nil, err + } + if !resp.Ok { + return nil, errors.New(resp.Error) + } + return resp.Profile, nil +} diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go index f28c94587..641bdf3ef 100644 --- a/vendor/github.com/nlopes/slack/websocket.go +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -3,6 +3,7 @@ package slack import ( "encoding/json" "errors" + "sync" "time" "github.com/gorilla/websocket" @@ -44,6 +45,13 @@ type RTM struct { // rtm.start to connect to Slack, otherwise it will use // rtm.connect useRTMStart bool + + // dialer is a gorilla/websocket Dialer. If nil, use the default + // Dialer. + dialer *websocket.Dialer + + // mu is mutex used to prevent RTM connection race conditions + mu *sync.Mutex } // RTMOptions allows configuration of various options available for RTM messaging @@ -60,6 +68,9 @@ type RTMOptions struct { // Disconnect and wait, blocking until a successful disconnection. func (rtm *RTM) Disconnect() error { + // avoid RTM disconnect race conditions + rtm.mu.Lock() + defer rtm.mu.Unlock() // this channel is always closed on disconnect. lets the ManagedConnection() function // properly clean up. close(rtm.disconnected) diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go index 7f7f35323..a78b3412b 100644 --- a/vendor/github.com/nlopes/slack/websocket_managed_conn.go +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -25,18 +25,26 @@ import ( // // The defined error events are located in websocket_internals.go. func (rtm *RTM) ManageConnection() { - var connectionCount int + var ( + err error + connectionCount int + info *Info + conn *websocket.Conn + ) + for { + // BEGIN SENSITIVE CODE, make sure lock is unlocked in this section. + rtm.mu.Lock() connectionCount++ // start trying to connect // the returned err is already passed onto the IncomingEvents channel - info, conn, err := rtm.connect(connectionCount, rtm.useRTMStart) - // if err != nil then the connection is sucessful - otherwise it is - // fatal - if err != nil { + if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil { + // when the connection is unsuccessful its fatal, and we need to bail out. rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err) + rtm.mu.Unlock() return } + rtm.info = info rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ ConnectionCount: connectionCount, @@ -45,6 +53,8 @@ func (rtm *RTM) ManageConnection() { rtm.conn = conn rtm.isConnected = true + rtm.mu.Unlock() + // END SENSITIVE CODE rtm.Debugf("RTM connection succeeded on try %d", connectionCount) @@ -71,6 +81,12 @@ func (rtm *RTM) ManageConnection() { // If useRTMStart is false then it uses rtm.connect to create the connection, // otherwise it uses rtm.start. func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocket.Conn, error) { + const ( + errInvalidAuth = "invalid_auth" + errInactiveAccount = "account_inactive" + errMissingAuthToken = "not_authed" + ) + // used to provide exponential backoff wait time with jitter before trying // to connect to slack again boff := &backoff{ @@ -91,11 +107,14 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke if err == nil { return info, conn, nil } - // check for fatal errors - currently only invalid_auth - if sErr, ok := err.(*WebError); ok && (sErr.Error() == "invalid_auth" || sErr.Error() == "account_inactive") { + + // check for fatal errors + switch err.Error() { + case errInvalidAuth, errInactiveAccount, errMissingAuthToken: rtm.Debugf("Invalid auth when connecting with RTM: %s", err) rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} - return nil, nil, sErr + return nil, nil, err + default: } // any other errors are treated as recoverable and we try again after @@ -107,7 +126,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // check if Disconnect() has been invoked. select { - case _ = <-rtm.disconnected: + case <-rtm.disconnected: rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: true}} return nil, nil, fmt.Errorf("disconnect received while trying to connect") default: @@ -124,10 +143,10 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // startRTMAndDial attempts to connect to the slack websocket. If useRTMStart is true, // then it returns the full information returned by the "rtm.start" method on the // slack API. Else it uses the "rtm.connect" method to connect -func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error) { - var info *Info - var url string - var err error +func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn, err error) { + var ( + url string + ) if useRTMStart { rtm.Debugf("Starting RTM") @@ -145,7 +164,11 @@ func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error // Only use HTTPS for connections to prevent MITM attacks on the connection. upgradeHeader := http.Header{} upgradeHeader.Add("Origin", "https://api.slack.com") - conn, _, err := websocket.DefaultDialer.Dial(url, upgradeHeader) + dialer := websocket.DefaultDialer + if rtm.dialer != nil { + dialer = rtm.dialer + } + conn, _, err := dialer.Dial(url, upgradeHeader) if err != nil { rtm.Debugf("Failed to dial to the websocket: %s", err) return nil, nil, err @@ -220,7 +243,9 @@ func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) { case <-keepRunning: return default: - rtm.receiveIncomingEvent() + if err := rtm.receiveIncomingEvent(); err != nil { + return + } } } } @@ -283,29 +308,33 @@ func (rtm *RTM) ping() error { // receiveIncomingEvent attempts to receive an event from the RTM's websocket. // This will block until a frame is available from the websocket. -func (rtm *RTM) receiveIncomingEvent() { +// If the read from the websocket results in a fatal error, this function will return non-nil. +func (rtm *RTM) receiveIncomingEvent() error { event := json.RawMessage{} err := rtm.conn.ReadJSON(&event) - if err == io.EOF { + switch { + case err == io.ErrUnexpectedEOF: // EOF's don't seem to signify a failed connection so instead we ignore // them here and detect a failed connection upon attempting to send a // 'PING' message - // trigger a 'PING' to detect pontential websocket disconnect + // trigger a 'PING' to detect potential websocket disconnect rtm.forcePing <- true - return - } else if err != nil { + case err != nil: + // All other errors from ReadJSON come from NextReader, and should + // kill the read loop and force a reconnect. rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{ ErrorObj: err, }} - // force a ping here too? - return - } else if len(event) == 0 { + rtm.killChannel <- false + return err + case len(event) == 0: rtm.Debugln("Received empty event") - return + default: + rtm.Debugln("Incoming Event:", string(event[:])) + rtm.rawEvents <- event } - rtm.Debugln("Incoming Event:", string(event[:])) - rtm.rawEvents <- event + return nil } // handleRawEvent takes a raw JSON message received from the slack websocket @@ -381,7 +410,7 @@ func (rtm *RTM) handlePong(event json.RawMessage) { // correct struct then this sends an UnmarshallingErrorEvent to the // IncomingEvents channel. func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { - v, exists := eventMapping[typeStr] + v, exists := EventMapping[typeStr] if !exists { rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) @@ -400,10 +429,10 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent} } -// eventMapping holds a mapping of event names to their corresponding struct +// EventMapping holds a mapping of event names to their corresponding struct // implementations. The structs should be instances of the unmarshalling // target for the matching event type. -var eventMapping = map[string]interface{}{ +var EventMapping = map[string]interface{}{ "message": MessageEvent{}, "presence_change": PresenceChangeEvent{}, "user_typing": UserTypingEvent{}, @@ -481,4 +510,7 @@ var eventMapping = map[string]interface{}{ "accounts_changed": AccountsChangedEvent{}, "reconnect_url": ReconnectUrlEvent{}, + + "member_joined_channel": MemberJoinedChannelEvent{}, + "member_left_channel": MemberLeftChannelEvent{}, } diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go index ad283ea1f..16f48c748 100644 --- a/vendor/github.com/nlopes/slack/websocket_misc.go +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -80,7 +80,7 @@ type EmojiChangedEvent struct { SubType string `json:"subtype"` Name string `json:"name"` Names []string `json:"names"` - Value string `json:"value"` + Value string `json:"value"` EventTimestamp string `json:"event_ts"` } @@ -119,3 +119,22 @@ type ReconnectUrlEvent struct { Type string `json:"type"` URL string `json:"url"` } + +// MemberJoinedChannelEvent, a user joined a public or private channel +type MemberJoinedChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` + Inviter string `json:"inviter"` +} + +// MemberJoinedChannelEvent, a user left a public or private channel +type MemberLeftChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` +} diff --git a/vendor/github.com/olekukonko/tablewriter/table.go b/vendor/github.com/olekukonko/tablewriter/table.go index d2db51909..6bbef96a8 100644 --- a/vendor/github.com/olekukonko/tablewriter/table.go +++ b/vendor/github.com/olekukonko/tablewriter/table.go @@ -9,6 +9,7 @@ package tablewriter import ( + "bytes" "fmt" "io" "regexp" @@ -20,95 +21,135 @@ const ( ) const ( - CENTRE = "+" - ROW = "-" - COLUMN = "|" - SPACE = " " + CENTER = "+" + ROW = "-" + COLUMN = "|" + SPACE = " " + NEWLINE = "\n" ) const ( ALIGN_DEFAULT = iota - ALIGN_CENTRE + ALIGN_CENTER ALIGN_RIGHT ALIGN_LEFT ) var ( - decimal = regexp.MustCompile(`^\d*\.?\d*$`) - percent = regexp.MustCompile(`^\d*\.?\d*$%$`) + decimal = regexp.MustCompile(`^-*\d*\.?\d*$`) + percent = regexp.MustCompile(`^-*\d*\.?\d*$%$`) ) +type Border struct { + Left bool + Right bool + Top bool + Bottom bool +} + type Table struct { - out io.Writer - rows [][]string - lines [][][]string - cs map[int]int - rs map[int]int - headers []string - footers []string - autoFmt bool - autoWrap bool - mW int - pCenter string - pRow string - pColumn string - tColumn int - tRow int - align int - rowLine bool - hdrLine bool - border bool - colSize int + out io.Writer + rows [][]string + lines [][][]string + cs map[int]int + rs map[int]int + headers [][]string + footers [][]string + caption bool + captionText string + autoFmt bool + autoWrap bool + reflowText bool + mW int + pCenter string + pRow string + pColumn string + tColumn int + tRow int + hAlign int + fAlign int + align int + newLine string + rowLine bool + autoMergeCells bool + hdrLine bool + borders Border + colSize int + headerParams []string + columnsParams []string + footerParams []string + columnsAlign []int } // Start New Table // Take io.Writer Directly func NewWriter(writer io.Writer) *Table { t := &Table{ - out: writer, - rows: [][]string{}, - lines: [][][]string{}, - cs: make(map[int]int), - rs: make(map[int]int), - headers: []string{}, - footers: []string{}, - autoFmt: true, - autoWrap: true, - mW: MAX_ROW_WIDTH, - pCenter: CENTRE, - pRow: ROW, - pColumn: COLUMN, - tColumn: -1, - tRow: -1, - align: ALIGN_DEFAULT, - rowLine: false, - hdrLine: true, - border: true, - colSize: -1} + out: writer, + rows: [][]string{}, + lines: [][][]string{}, + cs: make(map[int]int), + rs: make(map[int]int), + headers: [][]string{}, + footers: [][]string{}, + caption: false, + captionText: "Table caption.", + autoFmt: true, + autoWrap: true, + reflowText: true, + mW: MAX_ROW_WIDTH, + pCenter: CENTER, + pRow: ROW, + pColumn: COLUMN, + tColumn: -1, + tRow: -1, + hAlign: ALIGN_DEFAULT, + fAlign: ALIGN_DEFAULT, + align: ALIGN_DEFAULT, + newLine: NEWLINE, + rowLine: false, + hdrLine: true, + borders: Border{Left: true, Right: true, Bottom: true, Top: true}, + colSize: -1, + headerParams: []string{}, + columnsParams: []string{}, + footerParams: []string{}, + columnsAlign: []int{}} return t } // Render table output -func (t Table) Render() { - if t.border { +func (t *Table) Render() { + if t.borders.Top { t.printLine(true) } t.printHeading() - t.printRows() - - if !t.rowLine && t.border { + if t.autoMergeCells { + t.printRowsMergeCells() + } else { + t.printRows() + } + if !t.rowLine && t.borders.Bottom { t.printLine(true) } t.printFooter() + if t.caption { + t.printCaption() + } } +const ( + headerRowIdx = -1 + footerRowIdx = -2 +) + // Set table header func (t *Table) SetHeader(keys []string) { t.colSize = len(keys) for i, v := range keys { - t.parseDimension(v, i, -1) - t.headers = append(t.headers, v) + lines := t.parseDimension(v, i, headerRowIdx) + t.headers = append(t.headers, lines) } } @@ -116,8 +157,16 @@ func (t *Table) SetHeader(keys []string) { func (t *Table) SetFooter(keys []string) { //t.colSize = len(keys) for i, v := range keys { - t.parseDimension(v, i, -1) - t.footers = append(t.footers, v) + lines := t.parseDimension(v, i, footerRowIdx) + t.footers = append(t.footers, lines) + } +} + +// Set table Caption +func (t *Table) SetCaption(caption bool, captionText ...string) { + t.caption = caption + if len(captionText) == 1 { + t.captionText = captionText[0] } } @@ -131,11 +180,21 @@ func (t *Table) SetAutoWrapText(auto bool) { t.autoWrap = auto } +// Turn automatic reflowing of multiline text when rewrapping. Default is on (true). +func (t *Table) SetReflowDuringAutoWrap(auto bool) { + t.reflowText = auto +} + // Set the Default column width func (t *Table) SetColWidth(width int) { t.mW = width } +// Set the minimal width for a column +func (t *Table) SetColMinWidth(column int, width int) { + t.cs[column] = width +} + // Set the Column Separator func (t *Table) SetColumnSeparator(sep string) { t.pColumn = sep @@ -151,11 +210,42 @@ func (t *Table) SetCenterSeparator(sep string) { t.pCenter = sep } +// Set Header Alignment +func (t *Table) SetHeaderAlignment(hAlign int) { + t.hAlign = hAlign +} + +// Set Footer Alignment +func (t *Table) SetFooterAlignment(fAlign int) { + t.fAlign = fAlign +} + // Set Table Alignment func (t *Table) SetAlignment(align int) { t.align = align } +func (t *Table) SetColumnAlignment(keys []int) { + for _, v := range keys { + switch v { + case ALIGN_CENTER: + break + case ALIGN_LEFT: + break + case ALIGN_RIGHT: + break + default: + v = ALIGN_DEFAULT + } + t.columnsAlign = append(t.columnsAlign, v) + } +} + +// Set New Line +func (t *Table) SetNewLine(nl string) { + t.newLine = nl +} + // Set Header Line // This would enable / disable a line after the header func (t *Table) SetHeaderLine(line bool) { @@ -168,10 +258,20 @@ func (t *Table) SetRowLine(line bool) { t.rowLine = line } +// Set Auto Merge Cells +// This would enable / disable the merge of cells with identical values +func (t *Table) SetAutoMergeCells(auto bool) { + t.autoMergeCells = auto +} + // Set Table Border // This would enable / disable line around the table func (t *Table) SetBorder(border bool) { - t.border = border + t.SetBorders(Border{border, border, border, border}) +} + +func (t *Table) SetBorders(border Border) { + t.borders = border } // Append row to table @@ -204,8 +304,23 @@ func (t *Table) AppendBulk(rows [][]string) { } } +// NumLines to get the number of lines +func (t *Table) NumLines() int { + return len(t.lines) +} + +// Clear rows +func (t *Table) ClearRows() { + t.lines = [][][]string{} +} + +// Clear footer +func (t *Table) ClearFooter() { + t.footers = [][]string{} +} + // Print line based on row width -func (t Table) printLine(nl bool) { +func (t *Table) printLine(nl bool) { fmt.Fprint(t.out, t.pCenter) for i := 0; i < len(t.cs); i++ { v := t.cs[i] @@ -216,80 +331,172 @@ func (t Table) printLine(nl bool) { t.pCenter) } if nl { - fmt.Fprintln(t.out) + fmt.Fprint(t.out, t.newLine) + } +} + +// Print line based on row width with our without cell separator +func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) { + fmt.Fprint(t.out, t.pCenter) + for i := 0; i < len(t.cs); i++ { + v := t.cs[i] + if i > len(displayCellSeparator) || displayCellSeparator[i] { + // Display the cell separator + fmt.Fprintf(t.out, "%s%s%s%s", + t.pRow, + strings.Repeat(string(t.pRow), v), + t.pRow, + t.pCenter) + } else { + // Don't display the cell separator for this cell + fmt.Fprintf(t.out, "%s%s", + strings.Repeat(" ", v+2), + t.pCenter) + } + } + if nl { + fmt.Fprint(t.out, t.newLine) } } +// Return the PadRight function if align is left, PadLeft if align is right, +// and Pad by default +func pad(align int) func(string, string, int) string { + padFunc := Pad + switch align { + case ALIGN_LEFT: + padFunc = PadRight + case ALIGN_RIGHT: + padFunc = PadLeft + } + return padFunc +} + // Print heading information -func (t Table) printHeading() { +func (t *Table) printHeading() { // Check if headers is available if len(t.headers) < 1 { return } - // Check if border is set - // Replace with space if not set - fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE)) - // Identify last column end := len(t.cs) - 1 - // Print Heading column - for i := 0; i <= end; i++ { - v := t.cs[i] - h := t.headers[i] - if t.autoFmt { - h = Title(h) + // Get pad function + padFunc := pad(t.hAlign) + + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.headerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[headerRowIdx] + + // Print Heading + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + + for y := 0; y <= end; y++ { + v := t.cs[y] + h := "" + if y < len(t.headers) && x < len(t.headers[y]) { + h = t.headers[y][x] + } + if t.autoFmt { + h = Title(h) + } + pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn) + + if is_esc_seq { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(h, SPACE, v), + t.headerParams[y]), pad) + } else { + fmt.Fprintf(t.out, " %s %s", + padFunc(h, SPACE, v), + pad) + } } - pad := ConditionString((i == end && !t.border), SPACE, t.pColumn) - fmt.Fprintf(t.out, " %s %s", - Pad(h, SPACE, v), - pad) + // Next line + fmt.Fprint(t.out, t.newLine) } - // Next line - fmt.Fprintln(t.out) if t.hdrLine { t.printLine(true) } } // Print heading information -func (t Table) printFooter() { +func (t *Table) printFooter() { // Check if headers is available if len(t.footers) < 1 { return } // Only print line if border is not set - if !t.border { + if !t.borders.Bottom { t.printLine(true) } - // Check if border is set - // Replace with space if not set - fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE)) // Identify last column end := len(t.cs) - 1 - // Print Heading column - for i := 0; i <= end; i++ { - v := t.cs[i] - f := t.footers[i] - if t.autoFmt { - f = Title(f) - } - pad := ConditionString((i == end && !t.border), SPACE, t.pColumn) + // Get pad function + padFunc := pad(t.fAlign) - if len(t.footers[i]) == 0 { - pad = SPACE + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.footerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[footerRowIdx] + + // Print Footer + erasePad := make([]bool, len(t.footers)) + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE)) + + for y := 0; y <= end; y++ { + v := t.cs[y] + f := "" + if y < len(t.footers) && x < len(t.footers[y]) { + f = t.footers[y][x] + } + if t.autoFmt { + f = Title(f) + } + pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn) + + if erasePad[y] || (x == 0 && len(f) == 0) { + pad = SPACE + erasePad[y] = true + } + + if is_esc_seq { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(f, SPACE, v), + t.footerParams[y]), pad) + } else { + fmt.Fprintf(t.out, " %s %s", + padFunc(f, SPACE, v), + pad) + } + + //fmt.Fprintf(t.out, " %s %s", + // padFunc(f, SPACE, v), + // pad) } - fmt.Fprintf(t.out, " %s %s", - Pad(f, SPACE, v), - pad) + // Next line + fmt.Fprint(t.out, t.newLine) + //t.printLine(true) } - // Next line - fmt.Fprintln(t.out) - //t.printLine(true) hasPrinted := false @@ -297,14 +504,14 @@ func (t Table) printFooter() { v := t.cs[i] pad := t.pRow center := t.pCenter - length := len(t.footers[i]) + length := len(t.footers[i][0]) if length > 0 { hasPrinted = true } // Set center to be space if length is 0 - if length == 0 && !t.border { + if length == 0 && !t.borders.Right { center = SPACE } @@ -318,14 +525,14 @@ func (t Table) printFooter() { pad = SPACE } // Ignore left space of it has printed before - if hasPrinted || t.border { + if hasPrinted || t.borders.Left { pad = t.pRow center = t.pCenter } // Change Center start position if center == SPACE { - if i < end && len(t.footers[i+1]) != 0 { + if i < end && len(t.footers[i+1][0]) != 0 { center = t.pCenter } } @@ -339,23 +546,54 @@ func (t Table) printFooter() { } - fmt.Fprintln(t.out) + fmt.Fprint(t.out, t.newLine) +} +// Print caption text +func (t Table) printCaption() { + width := t.getTableWidth() + paragraph, _ := WrapString(t.captionText, width) + for linecount := 0; linecount < len(paragraph); linecount++ { + fmt.Fprintln(t.out, paragraph[linecount]) + } +} + +// Calculate the total number of characters in a row +func (t Table) getTableWidth() int { + var chars int + for _, v := range t.cs { + chars += v + } + + // Add chars, spaces, seperators to calculate the total width of the table. + // ncols := t.colSize + // spaces := ncols * 2 + // seps := ncols + 1 + + return (chars + (3 * t.colSize) + 2) } func (t Table) printRows() { for i, lines := range t.lines { t.printRow(lines, i) } +} +func (t *Table) fillAlignment(num int) { + if len(t.columnsAlign) < num { + t.columnsAlign = make([]int, num) + for i := range t.columnsAlign { + t.columnsAlign[i] = t.align + } + } } // Print Row Information // Adjust column alignment based on type -func (t Table) printRow(columns [][]string, colKey int) { +func (t *Table) printRow(columns [][]string, rowIdx int) { // Get Maximum Height - max := t.rs[colKey] + max := t.rs[rowIdx] total := len(columns) // TODO Fix uneven col size @@ -367,9 +605,15 @@ func (t Table) printRow(columns [][]string, colKey int) { //} // Pad Each Height - // pads := []int{} pads := []int{} + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } + t.fillAlignment(total) + for i, line := range columns { length := len(line) pad := max - length @@ -383,15 +627,20 @@ func (t Table) printRow(columns [][]string, colKey int) { for y := 0; y < total; y++ { // Check if border is set - fmt.Fprint(t.out, ConditionString((!t.border && y == 0), SPACE, t.pColumn)) + fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) fmt.Fprintf(t.out, SPACE) str := columns[y][x] + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + // This would print alignment // Default alignment would use multiple configuration - switch t.align { - case ALIGN_CENTRE: // + switch t.columnsAlign[y] { + case ALIGN_CENTER: // fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) case ALIGN_RIGHT: fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) @@ -416,56 +665,169 @@ func (t Table) printRow(columns [][]string, colKey int) { } // Check if border is set // Replace with space if not set - fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE)) - fmt.Fprintln(t.out) + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + fmt.Fprint(t.out, t.newLine) } if t.rowLine { t.printLine(true) } +} +// Print the rows of the table and merge the cells that are identical +func (t *Table) printRowsMergeCells() { + var previousLine []string + var displayCellBorder []bool + var tmpWriter bytes.Buffer + for i, lines := range t.lines { + // We store the display of the current line in a tmp writer, as we need to know which border needs to be print above + previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine) + if i > 0 { //We don't need to print borders above first line + if t.rowLine { + t.printLineOptionalCellSeparators(true, displayCellBorder) + } + } + tmpWriter.WriteTo(t.out) + } + //Print the end of the table + if t.rowLine { + t.printLine(true) + } +} + +// Print Row Information to a writer and merge identical cells. +// Adjust column alignment based on type + +func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) { + // Get Maximum Height + max := t.rs[rowIdx] + total := len(columns) + + // Pad Each Height + pads := []int{} + + for i, line := range columns { + length := len(line) + pad := max - length + pads = append(pads, pad) + for n := 0; n < pad; n++ { + columns[i] = append(columns[i], " ") + } + } + + var displayCellBorder []bool + t.fillAlignment(total) + for x := 0; x < max; x++ { + for y := 0; y < total; y++ { + + // Check if border is set + fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) + + fmt.Fprintf(writer, SPACE) + + str := columns[y][x] + + if t.autoMergeCells { + //Store the full line to merge mutli-lines cells + fullLine := strings.Join(columns[y], " ") + if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" { + // If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty. + displayCellBorder = append(displayCellBorder, false) + str = "" + } else { + // First line or different content, keep the content and print the cell border + displayCellBorder = append(displayCellBorder, true) + } + } + + // This would print alignment + // Default alignment would use multiple configuration + switch t.columnsAlign[y] { + case ALIGN_CENTER: // + fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y])) + case ALIGN_RIGHT: + fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) + case ALIGN_LEFT: + fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) + default: + if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { + fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) + } else { + fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) + } + } + fmt.Fprintf(writer, SPACE) + } + // Check if border is set + // Replace with space if not set + fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE)) + fmt.Fprint(writer, t.newLine) + } + + //The new previous line is the current one + previousLine = make([]string, total) + for y := 0; y < total; y++ { + previousLine[y] = strings.Join(columns[y], " ") //Store the full line for multi-lines cells + } + //Returns the newly added line and wether or not a border should be displayed above. + return previousLine, displayCellBorder } func (t *Table) parseDimension(str string, colKey, rowKey int) []string { var ( - raw []string - max int + raw []string + maxWidth int ) - w := DisplayWidth(str) - // Calculate Width - // Check if with is grater than maximum width - if w > t.mW { - w = t.mW - } - // Check if width exists - v, ok := t.cs[colKey] - if !ok || v < w || v == 0 { - t.cs[colKey] = w + raw = getLines(str) + maxWidth = 0 + for _, line := range raw { + if w := DisplayWidth(line); w > maxWidth { + maxWidth = w + } } - if rowKey == -1 { - return raw - } - // Calculate Height + // If wrapping, ensure that all paragraphs in the cell fit in the + // specified width. if t.autoWrap { - raw, _ = WrapString(str, t.cs[colKey]) - } else { - raw = getLines(str) - } + // If there's a maximum allowed width for wrapping, use that. + if maxWidth > t.mW { + maxWidth = t.mW + } - for _, line := range raw { - if w := DisplayWidth(line); w > max { - max = w + // In the process of doing so, we need to recompute maxWidth. This + // is because perhaps a word in the cell is longer than the + // allowed maximum width in t.mW. + newMaxWidth := maxWidth + newRaw := make([]string, 0, len(raw)) + + if t.reflowText { + // Make a single paragraph of everything. + raw = []string{strings.Join(raw, " ")} } + for i, para := range raw { + paraLines, _ := WrapString(para, maxWidth) + for _, line := range paraLines { + if w := DisplayWidth(line); w > newMaxWidth { + newMaxWidth = w + } + } + if i > 0 { + newRaw = append(newRaw, " ") + } + newRaw = append(newRaw, paraLines...) + } + raw = newRaw + maxWidth = newMaxWidth } - // Make sure the with is the same length as maximum word - // Important for cases where the width is smaller than maxu word - if max > t.cs[colKey] { - t.cs[colKey] = max + // Store the new known maximum width. + v, ok := t.cs[colKey] + if !ok || v < maxWidth || v == 0 { + t.cs[colKey] = maxWidth } + // Remember the number of lines for the row printer. h := len(raw) v, ok = t.rs[rowKey] diff --git a/vendor/github.com/olekukonko/tablewriter/table_with_color.go b/vendor/github.com/olekukonko/tablewriter/table_with_color.go new file mode 100644 index 000000000..5a4a53ec2 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table_with_color.go @@ -0,0 +1,134 @@ +package tablewriter + +import ( + "fmt" + "strconv" + "strings" +) + +const ESC = "\033" +const SEP = ";" + +const ( + BgBlackColor int = iota + 40 + BgRedColor + BgGreenColor + BgYellowColor + BgBlueColor + BgMagentaColor + BgCyanColor + BgWhiteColor +) + +const ( + FgBlackColor int = iota + 30 + FgRedColor + FgGreenColor + FgYellowColor + FgBlueColor + FgMagentaColor + FgCyanColor + FgWhiteColor +) + +const ( + BgHiBlackColor int = iota + 100 + BgHiRedColor + BgHiGreenColor + BgHiYellowColor + BgHiBlueColor + BgHiMagentaColor + BgHiCyanColor + BgHiWhiteColor +) + +const ( + FgHiBlackColor int = iota + 90 + FgHiRedColor + FgHiGreenColor + FgHiYellowColor + FgHiBlueColor + FgHiMagentaColor + FgHiCyanColor + FgHiWhiteColor +) + +const ( + Normal = 0 + Bold = 1 + UnderlineSingle = 4 + Italic +) + +type Colors []int + +func startFormat(seq string) string { + return fmt.Sprintf("%s[%sm", ESC, seq) +} + +func stopFormat() string { + return fmt.Sprintf("%s[%dm", ESC, Normal) +} + +// Making the SGR (Select Graphic Rendition) sequence. +func makeSequence(codes []int) string { + codesInString := []string{} + for _, code := range codes { + codesInString = append(codesInString, strconv.Itoa(code)) + } + return strings.Join(codesInString, SEP) +} + +// Adding ANSI escape sequences before and after string +func format(s string, codes interface{}) string { + var seq string + + switch v := codes.(type) { + + case string: + seq = v + case []int: + seq = makeSequence(v) + default: + return s + } + + if len(seq) == 0 { + return s + } + return startFormat(seq) + s + stopFormat() +} + +// Adding header colors (ANSI codes) +func (t *Table) SetHeaderColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of header colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.headerParams = append(t.headerParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetColumnColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of column colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.columnsParams = append(t.columnsParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetFooterColor(colors ...Colors) { + if len(t.footers) != len(colors) { + panic("Number of footer colors must be equal to number of footer.") + } + for i := 0; i < len(colors); i++ { + t.footerParams = append(t.footerParams, makeSequence(colors[i])) + } +} + +func Color(colors ...int) []int { + return colors +} diff --git a/vendor/github.com/olekukonko/tablewriter/util.go b/vendor/github.com/olekukonko/tablewriter/util.go index 2deefbc52..9e8f0cbb6 100644 --- a/vendor/github.com/olekukonko/tablewriter/util.go +++ b/vendor/github.com/olekukonko/tablewriter/util.go @@ -30,12 +30,33 @@ func ConditionString(cond bool, valid, inValid string) string { return inValid } +func isNumOrSpace(r rune) bool { + return ('0' <= r && r <= '9') || r == ' ' +} + // Format Table Header // Replace _ , . and spaces func Title(name string) string { - name = strings.Replace(name, "_", " ", -1) - name = strings.Replace(name, ".", " ", -1) + origLen := len(name) + rs := []rune(name) + for i, r := range rs { + switch r { + case '_': + rs[i] = ' ' + case '.': + // ignore floating number 0.0 + if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) { + rs[i] = ' ' + } + } + } + name = string(rs) name = strings.TrimSpace(name) + if len(name) == 0 && origLen > 0 { + // Keep at least one character. This is important to preserve + // empty lines in multi-line headers/footers. + name = " " + } return strings.ToUpper(name) } diff --git a/vendor/github.com/olekukonko/tablewriter/wrap.go b/vendor/github.com/olekukonko/tablewriter/wrap.go index f3747d9f3..a092ee1f7 100644 --- a/vendor/github.com/olekukonko/tablewriter/wrap.go +++ b/vendor/github.com/olekukonko/tablewriter/wrap.go @@ -10,7 +10,8 @@ package tablewriter import ( "math" "strings" - "unicode/utf8" + + "github.com/mattn/go-runewidth" ) var ( @@ -23,11 +24,11 @@ const defaultPenalty = 1e5 // Wrap wraps s into a paragraph of lines of length lim, with minimal // raggedness. func WrapString(s string, lim int) ([]string, int) { - words := strings.Split(strings.Replace(strings.TrimSpace(s), nl, sp, -1), sp) + words := strings.Split(strings.Replace(s, nl, sp, -1), sp) var lines []string max := 0 for _, v := range words { - max = len(v) + max = runewidth.StringWidth(v) if max > lim { lim = max } @@ -55,9 +56,9 @@ func WrapWords(words []string, spc, lim, pen int) [][]string { length := make([][]int, n) for i := 0; i < n; i++ { length[i] = make([]int, n) - length[i][i] = utf8.RuneCountInString(words[i]) + length[i][i] = runewidth.StringWidth(words[i]) for j := i + 1; j < n; j++ { - length[i][j] = length[i][j-1] + spc + utf8.RuneCountInString(words[j]) + length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j]) } } nbrk := make([]int, n) @@ -94,10 +95,5 @@ func WrapWords(words []string, spc, lim, pen int) [][]string { // getLines decomposes a multiline string into a slice of strings. func getLines(s string) []string { - var lines []string - - for _, line := range strings.Split(strings.TrimSpace(s), nl) { - lines = append(lines, line) - } - return lines + return strings.Split(s, nl) } diff --git a/vendor/github.com/spf13/pflag/flag.go b/vendor/github.com/spf13/pflag/flag.go index 5eadc84e3..5cc710ccd 100644 --- a/vendor/github.com/spf13/pflag/flag.go +++ b/vendor/github.com/spf13/pflag/flag.go @@ -990,11 +990,12 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin } func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parseFunc) (outShorts string, outArgs []string, err error) { + outArgs = args + if strings.HasPrefix(shorthands, "test.") { return } - outArgs = args outShorts = shorthands[1:] c := shorthands[0] diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go index d475a2fe1..bd1288ff6 100644 --- a/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "io/ioutil" + "math/rand" "mime" "net/http" "net/url" @@ -18,6 +19,7 @@ import ( "reflect" "strconv" "strings" + "time" "github.com/google/go-querystring/query" ) @@ -32,17 +34,33 @@ type HTTPClient interface { Do(req *http.Request) (res *http.Response, err error) } +// LimitData contains the latest Rate Limit or Flood Control data sent with every API call response. +// +// Limit is the number of API calls per period of time +// Remaining is the current number of API calls that can be done before the ResetTime +// ResetTime is the UTC time in Unix epoch format for when the full Limit of API calls will be restored. +type LimitData struct { + Limit int + Remaining int + ResetTime int +} + // Client manages the communication with the HipChat API. +// +// LatestFloodControl contains the response from the latest API call's response headers X-Floodcontrol-{Limit, Remaining, ResetTime} +// LatestRateLimit contains the response from the latest API call's response headers X-Ratelimit-{Limit, Remaining, ResetTime} +// Room gives access to the /room part of the API. +// User gives access to the /user part of the API. +// Emoticon gives access to the /emoticon part of the API. type Client struct { - authToken string - BaseURL *url.URL - client HTTPClient - // Room gives access to the /room part of the API. - Room *RoomService - // User gives access to the /user part of the API. - User *UserService - // Emoticon gives access to the /emoticon part of the API. - Emoticon *EmoticonService + authToken string + BaseURL *url.URL + client HTTPClient + LatestFloodControl LimitData + LatestRateLimit LimitData + Room *RoomService + User *UserService + Emoticon *EmoticonService } // Links represents the HipChat default links. @@ -63,23 +81,41 @@ type ID struct { ID string `json:"id"` } -// ListOptions specifies the optional parameters to various List methods that +// ListOptions specifies the optional parameters to various List methods that // support pagination. +// +// For paginated results, StartIndex represents the first page to display. +// For paginated results, MaxResults reprensents the number of items per page. Default value is 100. Maximum value is 1000. type ListOptions struct { - // For paginated results, represents the first page to display. StartIndex int `url:"start-index,omitempty"` - // For paginated results, reprensents the number of items per page. MaxResults int `url:"max-results,omitempty"` } +// ExpandOptions specifies which Hipchat collections to automatically expand. +// This functionality is primarily used to reduce the total time to receive the data. +// It also reduces the sheer number of API calls from 1+N, to 1. +// +// cf: https://developer.atlassian.com/hipchat/guide/hipchat-rest-api/api-title-expansion +type ExpandOptions struct { + Expand string `url:"expand,omitempty"` +} + +// Color is set of hard-coded string values for the HipChat API for notifications. +// cf: https://www.hipchat.com/docs/apiv2/method/send_room_notification type Color string const ( + // ColorYellow is the color yellow ColorYellow Color = "yellow" - ColorGreen Color = "green" - ColorRed Color = "red" + // ColorGreen is the color green + ColorGreen Color = "green" + // ColorRed is the color red + ColorRed Color = "red" + // ColorPurple is the color purple ColorPurple Color = "purple" - ColorGray Color = "gray" + // ColorGray is the color gray + ColorGray Color = "gray" + // ColorRandom is the random "surprise me!" color ColorRandom Color = "random" ) @@ -92,6 +128,46 @@ var AuthTest = false // API calls if AuthTest=true. var AuthTestResponse = map[string]interface{}{} +// RetryOnRateLimit can be set to true to automatically retry the API call until it succeeds, +// subject to the RateLimitRetryPolicy settings. This behavior is only active when the API +// call returns 429 (StatusTooManyRequests). +var RetryOnRateLimit = false + +// RetryPolicy defines a RetryPolicy. +// +// MaxRetries is the maximum number of attempts to make before returning an error +// MinDelay is the initial delay between attempts. This value is multiplied by the current attempt number. +// MaxDelay is the largest delay between attempts. +// JitterDelay is the amount of random jitter to add to the delay. +// JitterBias is the amount of jitter to remove from the delay. +// +// The use of Jitter avoids inadvertant and undesirable synchronization of network +// operations between otherwise unrelated clients. +// cf: https://brooker.co.za/blog/2015/03/21/backoff.html and https://www.awsarchitectureblog.com/2015/03/backoff.html +// +// Using the values of JitterDelay = 250 milliseconds and a JitterBias of negative 125 milliseconds, +// would result in a uniformly distributed Jitter between -125 and +125 milliseconds, centered +// around the current trial Delay (between MinDelay and MaxDelay). +// +// +type RetryPolicy struct { + MaxRetries int + MinDelay time.Duration + MaxDelay time.Duration + JitterDelay time.Duration + JitterBias time.Duration +} + +// NoRateLimitRetryPolicy defines the "never retry an API call" policy's values. +var NoRateLimitRetryPolicy = RetryPolicy{0, 1 * time.Second, 1 * time.Second, 500 * time.Millisecond, 0 * time.Millisecond} + +// DefaultRateLimitRetryPolicy defines the "up to 300 times, 1 second apart, randomly adding an additional up-to-500 milliseconds of delay" policy. +var DefaultRateLimitRetryPolicy = RetryPolicy{300, 1 * time.Second, 1 * time.Second, 500 * time.Millisecond, 0 * time.Millisecond} + +// RateLimitRetryPolicy can be set to a custom RetryPolicy's values, +// or to one of the two predefined ones: NoRateLimitRetryPolicy or DefaultRateLimitRetryPolicy +var RateLimitRetryPolicy = DefaultRateLimitRetryPolicy + // NewClient returns a new HipChat API client. You must provide a valid // AuthToken retrieved from your HipChat account. func NewClient(authToken string) *Client { @@ -220,7 +296,10 @@ func (c *Client) NewFileUploadRequest(method, urlStr string, v interface{}) (*ht "--hipfileboundary\n" b := &bytes.Buffer{} - b.Write([]byte(body)) + _, err = b.Write([]byte(body)) + if err != nil { + return nil, err + } req, err := http.NewRequest(method, u.String(), b) if err != nil { @@ -235,10 +314,15 @@ func (c *Client) NewFileUploadRequest(method, urlStr string, v interface{}) (*ht // Do performs the request, the json received in the response is decoded // and stored in the value pointed by v. -// Do can be used to perform the request created with NewRequest, as the latter -// it should be used only for API requests not implemented in this library. +// Do can be used to perform the request created with NewRequest, which +// should be used only for API requests not implemented in this library. func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { - resp, err := c.client.Do(req) + var policy = NoRateLimitRetryPolicy + if RetryOnRateLimit { + policy = RateLimitRetryPolicy + } + + resp, err := c.doWithRetryPolicy(req, policy) if err != nil { return nil, err } @@ -255,7 +339,7 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { if v != nil { defer resp.Body.Close() if w, ok := v.(io.Writer); ok { - io.Copy(w, resp.Body) + _, err = io.Copy(w, resp.Body) } else { err = json.NewDecoder(resp.Body).Decode(v) } @@ -264,6 +348,66 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { return resp, err } +func (c *Client) doWithRetryPolicy(req *http.Request, policy RetryPolicy) (*http.Response, error) { + currentTry := 0 + + for willContinue(currentTry, policy) { + currentTry = currentTry + 1 + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + c.captureRateLimits(resp) + if http.StatusTooManyRequests == resp.StatusCode { + resp.Body.Close() + if willContinue(currentTry, policy) { + sleep(currentTry, policy) + } + } else { + return resp, nil + } + } + return nil, fmt.Errorf("max retries exceeded (%d)", policy.MaxRetries) +} + +func willContinue(currentTry int, policy RetryPolicy) bool { + return currentTry <= policy.MaxRetries +} + +func sleep(currentTry int, policy RetryPolicy) { + jitter := time.Duration(rand.Int63n(2*int64(policy.JitterDelay))) - policy.JitterBias + linearDelay := time.Duration(currentTry)*policy.MinDelay + jitter + if linearDelay > policy.MaxDelay { + linearDelay = policy.MaxDelay + } + time.Sleep(time.Duration(linearDelay)) +} + +func setIfPresent(src string, dest *int) { + if len(src) > 0 { + v, err := strconv.Atoi(src) + if err != nil { + *dest = v + } + } +} + +func (c *Client) captureRateLimits(resp *http.Response) { + // BY DESIGN: + // if and only if the HTTP Response headers contain the header are the values updated. + // The Floodcontrol limits are orthogonal to the API limits. + // API Limits are consumed for each and every API call. + // The default value for API limits are 500 (app token) or 100 (user token). + // Flood Control limits are consumed only when a user message, room message, or room notification is sent. + // The default value for Flood Control limits is 30 per minute per user token. + setIfPresent(resp.Header.Get("X-Ratelimit-Limit"), &c.LatestRateLimit.Limit) + setIfPresent(resp.Header.Get("X-Ratelimit-Remaining"), &c.LatestRateLimit.Remaining) + setIfPresent(resp.Header.Get("X-Ratelimit-Reset"), &c.LatestRateLimit.ResetTime) + setIfPresent(resp.Header.Get("X-Floodcontrol-Limit"), &c.LatestFloodControl.Limit) + setIfPresent(resp.Header.Get("X-Floodcontrol-Remaining"), &c.LatestFloodControl.Remaining) + setIfPresent(resp.Header.Get("X-Floodcontrol-Reset"), &c.LatestFloodControl.ResetTime) +} + // addOptions adds the parameters in opt as URL query parameters to s. opt // must be a struct whose fields may contain "url" tags. func addOptions(s string, opt interface{}) (*url.URL, error) { diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go index 9beb51d53..7211ba802 100644 --- a/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go @@ -72,9 +72,9 @@ func (c *Client) GenerateToken(credentials ClientCredentials, scopes []string) ( content, err := ioutil.ReadAll(resp.Body) var token OAuthAccessToken - json.Unmarshal(content, &token) + err = json.Unmarshal(content, &token) - return &token, resp, nil + return &token, resp, err } const ( diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go index a5e2f702e..50e1a6d64 100644 --- a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go @@ -25,7 +25,7 @@ type Room struct { ID int `json:"id"` Links RoomLinks `json:"links"` Name string `json:"name"` - XmppJid string `json:"xmpp_jid"` + XMPPJid string `json:"xmpp_jid"` Statistics RoomStatistics `json:"statistics"` Created string `json:"created"` IsArchived bool `json:"is_archived"` @@ -93,7 +93,7 @@ type Card struct { Format string `json:"format,omitempty"` URL string `json:"url,omitempty"` Title string `json:"title"` - Thumbnail *Icon `json:"thumbnail,omitempty"` + Thumbnail *Thumbnail `json:"thumbnail,omitempty"` Activity *Activity `json:"activity,omitempty"` Attributes []Attribute `json:"attributes,omitempty"` ID string `json:"id,omitempty"` @@ -182,7 +182,7 @@ type Thumbnail struct { URL string `json:"url"` URL2x string `json:"url@2x,omitempty"` Width uint `json:"width,omitempty"` - Height uint `json:"url,omitempty"` + Height uint `json:"height,omitempty"` } // Attribute represents an attribute on a Card @@ -243,12 +243,17 @@ type InviteRequest struct { Reason string `json:"reason"` } +// AddMemberRequest represents a HipChat add member request +type AddMemberRequest struct { + Roles []string `json:"roles,omitempty"` +} + // GlanceRequest represents a HipChat room ui glance type GlanceRequest struct { Key string `json:"key"` Name GlanceName `json:"name"` Target string `json:"target"` - QueryURL string `json:"queryUrl"` + QueryURL string `json:"queryUrl,omitempty"` Icon Icon `json:"icon"` Conditions []*GlanceCondition `json:"conditions,omitempty"` } @@ -279,7 +284,7 @@ type GlanceUpdate struct { // GlanceContent is a component of a Glance type GlanceContent struct { - Status GlanceStatus `json:"status"` + Status *GlanceStatus `json:"status,omitempty"` Metadata interface{} `json:"metadata,omitempty"` Label AttributeValue `json:"label"` // AttributeValue{Type, Label} } @@ -360,6 +365,7 @@ func (c *Card) AddAttribute(mainLabel, subLabel, url, iconURL string) { // method. type RoomsListOptions struct { ListOptions + ExpandOptions // Include private rooms in the result, API defaults to true IncludePrivate bool `url:"include-private,omitempty"` @@ -499,6 +505,7 @@ func (r *RoomService) Update(id string, roomReq *UpdateRoomRequest) (*http.Respo // HistoryOptions represents a HipChat room chat history request. type HistoryOptions struct { ListOptions + ExpandOptions // Either the latest date to fetch history for in ISO-8601 format, or 'recent' to fetch // the latest 75 messages. Paging isn't supported for 'recent', however they are real-time @@ -511,6 +518,14 @@ type HistoryOptions struct { // Reverse the output such that the oldest message is first. // For consistent paging, set to 'false'. Reverse bool `url:"reverse,omitempty"` + + // Either the earliest date to fetch history for the ISO-8601 format string, + // or leave blank to disable this filter. + // to be effective, the API call requires Date also be filled in with an ISO-8601 format string. + EndDate string `url:"end-date,omitempty"` + + // Include records about deleted messages into results (body of a message isn't returned). Set to 'true'. + IncludeDeleted bool `url:"include_deleted,omitempty"` } // History fetches a room's chat history. @@ -618,3 +633,27 @@ func (r *RoomService) UpdateGlance(id string, glanceUpdateReq *GlanceUpdateReque return r.client.Do(req, nil) } + +// AddMember adds a member to a private room and sends member's unavailable presence to all room members asynchronously. +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/add_member +func (r *RoomService) AddMember(roomID string, userID string, addMemberReq *AddMemberRequest) (*http.Response, error) { + req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/member/%s", roomID, userID), nil, addMemberReq) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} + +// RemoveMember removes a member from a private room +// +// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/remove_member +func (r *RoomService) RemoveMember(roomID string, userID string) (*http.Response, error) { + req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s/member/%s", roomID, userID), nil, nil) + if err != nil { + return nil, err + } + + return r.client.Do(req, nil) +} diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go index 81fa15007..c45872ec5 100644 --- a/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go +++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go @@ -50,7 +50,7 @@ type UpdateUserPresenceRequest struct { // User represents the HipChat user. type User struct { - XmppJid string `json:"xmpp_jid"` + XMPPJid string `json:"xmpp_jid"` IsDeleted bool `json:"is_deleted"` Name string `json:"name"` LastActive string `json:"last_active"` @@ -121,6 +121,7 @@ func (u *UserService) Message(id string, msgReq *MessageRequest) (*http.Response // UserListOptions specified the parameters to the UserService.List method. type UserListOptions struct { ListOptions + ExpandOptions // Include active guest users in response. IncludeGuests bool `url:"include-guests,omitempty"` // Include deleted users in response. diff --git a/vendor/k8s.io/apimachinery/pkg/api/meta/restmapper.go b/vendor/k8s.io/apimachinery/pkg/api/meta/restmapper.go index ff945acd1..45f18bab8 100644 --- a/vendor/k8s.io/apimachinery/pkg/api/meta/restmapper.go +++ b/vendor/k8s.io/apimachinery/pkg/api/meta/restmapper.go @@ -156,14 +156,67 @@ func UnsafeGuessKindToResource(kind schema.GroupVersionKind) ( /*plural*/ schema } } - switch string(singularName[len(singularName)-1]) { - case "s": - return kind.GroupVersion().WithResource(singularName + "es"), singular - case "y": - return kind.GroupVersion().WithResource(strings.TrimSuffix(singularName, "y") + "ies"), singular + var plural string + switch rune(singularName[len(singularName)-1]) { + case 's', 'x', 'z': + plural = esPlural(singularName) + case 'y': + sl := rune(singularName[len(singularName)-2]) + if isConsonant(sl) { + plural = iesPlural(singularName) + } else { + plural = sPlural(singularName) + } + case 'h': + sl := rune(singularName[len(singularName)-2]) + if sl == 'c' || sl == 's' { + plural = esPlural(singularName) + } else { + plural = sPlural(singularName) + } + case 'e': + sl := rune(singularName[len(singularName)-2]) + if sl == 'f' { + plural = vesPlural(singularName[:len(singularName)-1]) + } else { + plural = sPlural(singularName) + } + case 'f': + plural = vesPlural(singularName) + default: + plural = sPlural(singularName) } - return kind.GroupVersion().WithResource(singularName + "s"), singular + return kind.GroupVersion().WithResource(plural), singular +} + +// ref: https://github.com/kubernetes/gengo/blob/master/namer/plural_namer.go +// ref: https://github.com/kubernetes/apimachinery/issues/40 +var consonants = "bcdfghjklmnpqrsttvwxyz" + +func iesPlural(singular string) string { + return singular[:len(singular)-1] + "ies" +} + +func vesPlural(singular string) string { + return singular[:len(singular)-1] + "ves" +} + +func esPlural(singular string) string { + return singular + "es" +} + +func sPlural(singular string) string { + return singular + "s" +} + +func isConsonant(char rune) bool { + for _, c := range consonants { + if char == c { + return true + } + } + return false } // ResourceSingularizer implements RESTMapper diff --git a/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/duration.go b/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/duration.go index fea458dfb..a570e8c45 100644 --- a/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/duration.go +++ b/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/duration.go @@ -45,3 +45,13 @@ func (d *Duration) UnmarshalJSON(b []byte) error { func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(d.Duration.String()) } + +// OpenAPISchemaType is used by the kube-openapi generator when constructing +// the OpenAPI spec of this type. +// +// See: https://github.com/kubernetes/kube-openapi/tree/master/pkg/generators +func (_ Duration) OpenAPISchemaType() []string { return []string{"string"} } + +// OpenAPISchemaFormat is used by the kube-openapi generator when constructing +// the OpenAPI spec of this type. +func (_ Duration) OpenAPISchemaFormat() string { return "" } diff --git a/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/time.go b/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/time.go index 5041954f7..5f3d3838e 100644 --- a/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/time.go +++ b/vendor/k8s.io/apimachinery/pkg/apis/meta/v1/time.go @@ -20,6 +20,7 @@ import ( "encoding/json" "time" + "github.com/golang/protobuf/jsonpb" "github.com/google/gofuzz" ) @@ -148,6 +149,14 @@ func (t Time) MarshalJSON() ([]byte, error) { return json.Marshal(t.UTC().Format(time.RFC3339)) } +func (t Time) MarshalJSONPB(_ *jsonpb.Marshaler) ([]byte, error) { + return t.MarshalJSON() +} + +func (t *Time) UnmarshalJSONPB(_ *jsonpb.Unmarshaler, jstr []byte) error { + return t.UnmarshalJSON(jstr) +} + // OpenAPISchemaType is used by the kube-openapi generator when constructing // the OpenAPI spec of this type. // diff --git a/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go b/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go index e4fe7c62e..f48700d54 100644 --- a/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go +++ b/vendor/k8s.io/kube-openapi/pkg/builder/openapi.go @@ -58,7 +58,7 @@ func BuildOpenAPIDefinitionsForResource(model interface{}, config *common.Config o := newOpenAPI(config) // We can discard the return value of toSchema because all we care about is the side effect of calling it. // All the models created for this resource get added to o.swagger.Definitions - _, err := o.toSchema(model) + _, err := o.toSchema(getCanonicalTypeName(model)) if err != nil { return nil, err } @@ -69,6 +69,21 @@ func BuildOpenAPIDefinitionsForResource(model interface{}, config *common.Config return &swagger.Definitions, nil } +// BuildOpenAPIDefinitionsForResources returns the OpenAPI spec which includes the definitions for the +// passed type names. +func BuildOpenAPIDefinitionsForResources(config *common.Config, names ...string) (*spec.Swagger, error) { + o := newOpenAPI(config) + // We can discard the return value of toSchema because all we care about is the side effect of calling it. + // All the models created for this resource get added to o.swagger.Definitions + for _, name := range names { + _, err := o.toSchema(name) + if err != nil { + return nil, err + } + } + return o.finalizeSwagger() +} + // newOpenAPI sets up the openAPI object so we can build the spec. func newOpenAPI(config *common.Config) openAPI { o := openAPI{ @@ -120,7 +135,11 @@ func (o *openAPI) finalizeSwagger() (*spec.Swagger, error) { return o.swagger, nil } -func getCanonicalizeTypeName(t reflect.Type) string { +func getCanonicalTypeName(model interface{}) string { + t := reflect.TypeOf(model) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } if t.PkgPath() == "" { return t.Name() } @@ -165,12 +184,7 @@ func (o *openAPI) buildDefinitionRecursively(name string) error { // buildDefinitionForType build a definition for a given type and return a referable name to its definition. // This is the main function that keep track of definitions used in this spec and is depend on code generated // by k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen. -func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) { - t := reflect.TypeOf(sample) - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - name := getCanonicalizeTypeName(t) +func (o *openAPI) buildDefinitionForType(name string) (string, error) { if err := o.buildDefinitionRecursively(name); err != nil { return "", err } @@ -321,7 +335,7 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map } func (o *openAPI) buildResponse(model interface{}, description string) (spec.Response, error) { - schema, err := o.toSchema(model) + schema, err := o.toSchema(getCanonicalTypeName(model)) if err != nil { return spec.Response{}, err } @@ -366,8 +380,8 @@ func (o *openAPI) findCommonParameters(routes []restful.Route) (map[interface{}] return commonParamsMap, nil } -func (o *openAPI) toSchema(model interface{}) (_ *spec.Schema, err error) { - if openAPIType, openAPIFormat := common.GetOpenAPITypeFormat(getCanonicalizeTypeName(reflect.TypeOf(model))); openAPIType != "" { +func (o *openAPI) toSchema(name string) (_ *spec.Schema, err error) { + if openAPIType, openAPIFormat := common.GetOpenAPITypeFormat(name); openAPIType != "" { return &spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{openAPIType}, @@ -375,7 +389,7 @@ func (o *openAPI) toSchema(model interface{}) (_ *spec.Schema, err error) { }, }, nil } else { - ref, err := o.buildDefinitionForType(model) + ref, err := o.buildDefinitionForType(name) if err != nil { return nil, err } @@ -399,7 +413,7 @@ func (o *openAPI) buildParameter(restParam restful.ParameterData, bodySample int case restful.BodyParameterKind: if bodySample != nil { ret.In = "body" - ret.Schema, err = o.toSchema(bodySample) + ret.Schema, err = o.toSchema(getCanonicalTypeName(bodySample)) return ret, err } else { // There is not enough information in the body parameter to build the definition.