From ed11980784f485cad4c4ffaa18f7b261c9cd985f Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 24 Sep 2025 09:36:45 +0200
Subject: [PATCH] start websocket implementation, add endpoint for email
summaries
* feat(groupware): start implementing JMAP websocket support for push
notifications (unfinished)
* groupware: add GetLatestEmailsSummaryForAllAccounts
* add new vendored dependency: github.com/gorilla/websocket
* jmap: add QueryEmailSummaries
* openapi: start adding examples
* openapi: add new tooling for api-examples.yaml injection
* apidoc-process.ts: make it more typescript-y
* bump @redocly/cli from 2.0.8 to latest 2.2.0
---
go.mod | 1 +
go.sum | 2 +
pkg/jmap/jmap_api.go | 16 +
pkg/jmap/jmap_api_email.go | 64 +-
pkg/jmap/jmap_api_mailbox.go | 8 +-
pkg/jmap/jmap_api_ws.go | 13 +
pkg/jmap/jmap_client.go | 21 +-
pkg/jmap/jmap_error.go | 6 +
pkg/jmap/jmap_http.go | 153 ++
pkg/jmap/jmap_integration_test.go | 11 +-
pkg/jmap/jmap_model.go | 128 +-
pkg/jmap/jmap_session.go | 16 +-
pkg/jmap/jmap_test.go | 25 +-
services/groupware/api-examples.yaml | 69 +
services/groupware/apidoc-process.ts | 153 +-
services/groupware/package.json | 4 +-
services/groupware/pkg/config/config.go | 1 +
.../pkg/config/defaults/defaultconfig.go | 1 +
...pi_messages.go => groupware_api_emails.go} | 332 +++++
.../pkg/groupware/groupware_framework.go | 42 +-
.../pkg/groupware/groupware_route.go | 5 +
services/groupware/pnpm-lock.yaml | 45 +-
services/groupware/pnpm-workspace.yaml | 3 +
.../github.com/gorilla/websocket/.gitignore | 25 +
vendor/github.com/gorilla/websocket/AUTHORS | 9 +
vendor/github.com/gorilla/websocket/LICENSE | 22 +
vendor/github.com/gorilla/websocket/README.md | 33 +
vendor/github.com/gorilla/websocket/client.go | 434 ++++++
.../gorilla/websocket/compression.go | 148 ++
vendor/github.com/gorilla/websocket/conn.go | 1238 +++++++++++++++++
vendor/github.com/gorilla/websocket/doc.go | 227 +++
vendor/github.com/gorilla/websocket/join.go | 42 +
vendor/github.com/gorilla/websocket/json.go | 60 +
vendor/github.com/gorilla/websocket/mask.go | 55 +
.../github.com/gorilla/websocket/mask_safe.go | 16 +
.../github.com/gorilla/websocket/prepared.go | 102 ++
vendor/github.com/gorilla/websocket/proxy.go | 77 +
vendor/github.com/gorilla/websocket/server.go | 365 +++++
.../gorilla/websocket/tls_handshake.go | 21 +
.../gorilla/websocket/tls_handshake_116.go | 21 +
vendor/github.com/gorilla/websocket/util.go | 298 ++++
.../gorilla/websocket/x_net_proxy.go | 473 +++++++
vendor/modules.txt | 3 +
43 files changed, 4693 insertions(+), 95 deletions(-)
create mode 100644 pkg/jmap/jmap_api_ws.go
create mode 100644 services/groupware/api-examples.yaml
rename services/groupware/pkg/groupware/{groupware_api_messages.go => groupware_api_emails.go} (67%)
create mode 100644 services/groupware/pnpm-workspace.yaml
create mode 100644 vendor/github.com/gorilla/websocket/.gitignore
create mode 100644 vendor/github.com/gorilla/websocket/AUTHORS
create mode 100644 vendor/github.com/gorilla/websocket/LICENSE
create mode 100644 vendor/github.com/gorilla/websocket/README.md
create mode 100644 vendor/github.com/gorilla/websocket/client.go
create mode 100644 vendor/github.com/gorilla/websocket/compression.go
create mode 100644 vendor/github.com/gorilla/websocket/conn.go
create mode 100644 vendor/github.com/gorilla/websocket/doc.go
create mode 100644 vendor/github.com/gorilla/websocket/join.go
create mode 100644 vendor/github.com/gorilla/websocket/json.go
create mode 100644 vendor/github.com/gorilla/websocket/mask.go
create mode 100644 vendor/github.com/gorilla/websocket/mask_safe.go
create mode 100644 vendor/github.com/gorilla/websocket/prepared.go
create mode 100644 vendor/github.com/gorilla/websocket/proxy.go
create mode 100644 vendor/github.com/gorilla/websocket/server.go
create mode 100644 vendor/github.com/gorilla/websocket/tls_handshake.go
create mode 100644 vendor/github.com/gorilla/websocket/tls_handshake_116.go
create mode 100644 vendor/github.com/gorilla/websocket/util.go
create mode 100644 vendor/github.com/gorilla/websocket/x_net_proxy.go
diff --git a/go.mod b/go.mod
index 1cc79ace2b..ef970beb68 100644
--- a/go.mod
+++ b/go.mod
@@ -257,6 +257,7 @@ require (
github.com/gookit/goutil v0.7.1 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/schema v1.4.1 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.7.0 // indirect
diff --git a/go.sum b/go.sum
index 363e5ab541..7cb412ee05 100644
--- a/go.sum
+++ b/go.sum
@@ -641,6 +641,8 @@ github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWS
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
diff --git a/pkg/jmap/jmap_api.go b/pkg/jmap/jmap_api.go
index a8bfd45f95..24080ac476 100644
--- a/pkg/jmap/jmap_api.go
+++ b/pkg/jmap/jmap_api.go
@@ -13,13 +13,29 @@ type ApiClient interface {
io.Closer
}
+type WsPushListener interface {
+ OnNotification(stateChange StateChange)
+}
+
+type WsClient interface {
+ DisableNotifications() Error
+ io.Closer
+}
+
+type WsClientFactory interface {
+ EnableNotifications(pushState string, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error)
+ io.Closer
+}
+
type SessionClient interface {
GetSession(baseurl *url.URL, username string, logger *log.Logger) (SessionResponse, Error)
+ io.Closer
}
type BlobClient interface {
UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, content io.Reader) (UploadedBlob, Error)
DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string) (*BlobDownload, Error)
+ io.Closer
}
const (
diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index 1ec92265c6..8aeb73e48c 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/opencloud-eu/opencloud/pkg/log"
+ "github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
@@ -77,7 +78,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c
get := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"},
+ IdsRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
get.MaxBodyValueBytes = maxBodyValueBytes
@@ -131,7 +132,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
getCreated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandEmailChanges, Path: "/created", ResultOf: "0"},
+ IdsRef: &ResultReference{Name: CommandEmailChanges, Path: "/created", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getCreated.MaxBodyValueBytes = maxBodyValueBytes
@@ -139,7 +140,7 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
getUpdated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandEmailChanges, Path: "/updated", ResultOf: "0"},
+ IdsRef: &ResultReference{Name: CommandEmailChanges, Path: "/updated", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
@@ -284,7 +285,7 @@ func (j *Client) QueryEmails(accountId string, filter EmailFilterElement, sessio
mails := EmailGetRefCommand{
AccountId: accountId,
- IdRef: &ResultReference{
+ IdsRef: &ResultReference{
ResultOf: "0",
Name: CommandEmailQuery,
Path: "/ids/*",
@@ -369,7 +370,7 @@ func (j *Client) QueryEmailsWithSnippets(accountId string, filter EmailFilterEle
mails := EmailGetRefCommand{
AccountId: accountId,
- IdRef: &ResultReference{
+ IdsRef: &ResultReference{
ResultOf: "0",
Name: CommandEmailQuery,
Path: "/ids/*",
@@ -759,7 +760,7 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess
}, "0"),
invocation(CommandEmailGet, EmailGetRefCommand{
AccountId: accountId,
- IdRef: &ResultReference{
+ IdsRef: &ResultReference{
ResultOf: "0",
Name: CommandThreadGet,
Path: "/list/*/emailIds",
@@ -782,3 +783,54 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess
})
}
+
+type EmailsSummary struct {
+ Emails []Email `json:"emails"`
+ State State `json:"state"`
+}
+
+func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, filter EmailFilterElement, limit uint) (map[string]EmailsSummary, SessionState, Error) {
+ logger = j.logger("QueryEmailSummaries", session, logger)
+
+ uniqueAccountIds := structs.Uniq(accountIds)
+
+ invocations := make([]Invocation, len(uniqueAccountIds)*2)
+ for i, accountId := range uniqueAccountIds {
+ invocations[i*2+0] = invocation(CommandEmailQuery, EmailQueryCommand{
+ AccountId: accountId,
+ Filter: filter,
+ Sort: []EmailComparator{EmailComparator{Property: emailSortByReceivedAt, IsAscending: false}},
+ Limit: limit,
+ //CalculateTotal: false,
+ }, mcid(accountId, "0"))
+ invocations[i*2+1] = invocation(CommandEmailGet, EmailGetRefCommand{
+ AccountId: accountId,
+ IdsRef: &ResultReference{
+ Name: CommandEmailQuery,
+ Path: "/ids/*",
+ ResultOf: mcid(accountId, "0"),
+ },
+ Properties: []string{"id", "threadId", "mailboxIds", "keywords", "size", "receivedAt", "sender", "from", "to", "cc", "bcc", "subject", "sentAt", "hasAttachment", "attachments", "preview"},
+ }, mcid(accountId, "1"))
+ }
+ cmd, err := j.request(session, logger, invocations...)
+ if err != nil {
+ return map[string]EmailsSummary{}, "", err
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (map[string]EmailsSummary, Error) {
+ resp := map[string]EmailsSummary{}
+ for _, accountId := range uniqueAccountIds {
+ var response EmailGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandEmailGet, mcid(accountId, "1"), &response)
+ if err != nil {
+ return map[string]EmailsSummary{}, err
+ }
+ if len(response.NotFound) > 0 {
+ // TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
+ }
+ resp[accountId] = EmailsSummary{Emails: response.List, State: response.State}
+ }
+ return resp, nil
+ })
+}
diff --git a/pkg/jmap/jmap_api_mailbox.go b/pkg/jmap/jmap_api_mailbox.go
index 6c43e20b65..558b674280 100644
--- a/pkg/jmap/jmap_api_mailbox.go
+++ b/pkg/jmap/jmap_api_mailbox.go
@@ -152,7 +152,7 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte
getCreated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
+ IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getCreated.MaxBodyValueBytes = maxBodyValueBytes
@@ -160,7 +160,7 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte
getUpdated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
+ IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
@@ -241,7 +241,7 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi
getCreated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: mcid(accountId, "0")},
+ IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: mcid(accountId, "0")},
}
if maxBodyValueBytes > 0 {
getCreated.MaxBodyValueBytes = maxBodyValueBytes
@@ -249,7 +249,7 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, sessi
getUpdated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
- IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: mcid(accountId, "0")},
+ IdsRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: mcid(accountId, "0")},
}
if maxBodyValueBytes > 0 {
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
diff --git a/pkg/jmap/jmap_api_ws.go b/pkg/jmap/jmap_api_ws.go
new file mode 100644
index 0000000000..62cef36ce3
--- /dev/null
+++ b/pkg/jmap/jmap_api_ws.go
@@ -0,0 +1,13 @@
+package jmap
+
+import (
+ "github.com/opencloud-eu/opencloud/pkg/log"
+)
+
+func (j *Client) EnablePush(pushState string, session *Session, _ *log.Logger) Error {
+ return nil // TODO
+}
+
+func (j *Client) DisablePush(_ *Session, _ *log.Logger) Error {
+ return nil // TODO
+}
diff --git a/pkg/jmap/jmap_client.go b/pkg/jmap/jmap_client.go
index e8337e2d18..8268a0d0e3 100644
--- a/pkg/jmap/jmap_client.go
+++ b/pkg/jmap/jmap_client.go
@@ -1,6 +1,7 @@
package jmap
import (
+ "errors"
"io"
"net/url"
@@ -12,22 +13,28 @@ type Client struct {
session SessionClient
api ApiClient
blob BlobClient
+ ws WsClientFactory
sessionEventListeners *eventListeners[SessionEventListener]
+ wsPushListeners *eventListeners[WsPushListener]
io.Closer
+ WsPushListener
}
var _ io.Closer = &Client{}
+var _ WsPushListener = &Client{}
func (j *Client) Close() error {
- return j.api.Close()
+ return errors.Join(j.api.Close(), j.session.Close(), j.blob.Close(), j.ws.Close())
}
-func NewClient(session SessionClient, api ApiClient, blob BlobClient) Client {
+func NewClient(session SessionClient, api ApiClient, blob BlobClient, ws WsClientFactory) Client {
return Client{
session: session,
api: api,
blob: blob,
+ ws: ws,
sessionEventListeners: newEventListeners[SessionEventListener](),
+ wsPushListeners: newEventListeners[WsPushListener](),
}
}
@@ -41,6 +48,16 @@ func (j *Client) onSessionOutdated(session *Session, newSessionState SessionStat
})
}
+func (j *Client) AddWsPushListener(listener WsPushListener) {
+ j.wsPushListeners.add(listener)
+}
+
+func (j *Client) OnNotification(stateChange StateChange) {
+ j.wsPushListeners.signal(func(listener WsPushListener) {
+ listener.OnNotification(stateChange)
+ })
+}
+
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
func (j *Client) FetchSession(sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
wk, err := j.session.GetSession(sessionUrl, username, logger)
diff --git a/pkg/jmap/jmap_error.go b/pkg/jmap/jmap_error.go
index 710ff397bb..e81b9578af 100644
--- a/pkg/jmap/jmap_error.go
+++ b/pkg/jmap/jmap_error.go
@@ -30,6 +30,12 @@ const (
JmapErrorAccountNotFound
JmapErrorAccountNotSupportedByMethod
JmapErrorAccountReadOnly
+ JmapErrorFailedToEstablishWssConnection
+ JmapErrorWssConnectionResponseMissingJmapSubprotocol
+ JmapErrorWssFailedToSendWebSocketPushEnable
+ JmapErrorWssFailedToSendWebSocketPushDisable
+ JmapErrorWssFailedToClose
+ JmapErrorWssFailedToRetrieveSession
)
var (
diff --git a/pkg/jmap/jmap_http.go b/pkg/jmap/jmap_http.go
index bdc633bd37..62404d5565 100644
--- a/pkg/jmap/jmap_http.go
+++ b/pkg/jmap/jmap_http.go
@@ -3,14 +3,17 @@ package jmap
import (
"bytes"
"context"
+ "encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
+ "slices"
"strconv"
+ "github.com/gorilla/websocket"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/version"
)
@@ -320,3 +323,153 @@ func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger,
CacheControl: res.Header.Get("Cache-Control"),
}, nil
}
+
+type WebSocketPushEnable struct {
+ // This MUST be the string "WebSocketPushEnable".
+ Type string `json:"@type"`
+
+ // A list of data type names (e.g., "Mailbox" or "Email") that the client is interested in.
+ //
+ // A StateChange notification will only be sent if the data for one of these types changes.
+ // Other types are omitted from the TypeState object.
+ //
+ // If null, changes will be pushed for all supported data types.
+ DataTypes *[]string `json:"dataTypes"`
+
+ // The last "pushState" token that the client received from the server.
+
+ // Upon receipt of a "pushState" token, the server SHOULD immediately send all changes since that state token.
+ PushState string `json:"pushState,omitempty"`
+}
+
+type WebSocketPushDisable struct {
+ // This MUST be the string "WebSocketPushDisable".
+ Type string `json:"@type"`
+}
+
+type HttpWsClientFactory struct {
+ dialer *websocket.Dialer
+ masterUser string
+ masterPassword string
+}
+
+var _ WsClientFactory = &HttpWsClientFactory{}
+
+func NewHttpWsClientFactory(dialer *websocket.Dialer, masterUser string, masterPassword string, logger *log.Logger) (*HttpWsClientFactory, error) {
+ /*
+ d := websocket.Dialer{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // TODO configurable
+ HandshakeTimeout: 5 * time.Second,
+ // RFC 8887: Section 4.2:
+ // Otherwise, the client MUST make an authenticated HTTP request [RFC7235] on the encrypted connection
+ // and MUST include the value "jmap" in the list of protocols for the "Sec-WebSocket-Protocol" header
+ // field.
+ Subprotocols: []string{"jmap"},
+ }
+ */
+
+ // RFC 8887: Section 4.2:
+ // Otherwise, the client MUST make an authenticated HTTP request [RFC7235] on the encrypted connection
+ // and MUST include the value "jmap" in the list of protocols for the "Sec-WebSocket-Protocol" header
+ // field.
+ dialer.Subprotocols = []string{"jmap"}
+
+ return &HttpWsClientFactory{
+ dialer: dialer,
+ masterUser: masterUser,
+ masterPassword: masterPassword,
+ }, nil
+}
+
+func (w *HttpWsClientFactory) auth(username string, h http.Header) error {
+ masterUsername := username + "%" + w.masterUser
+ h.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(masterUsername+":"+w.masterPassword)))
+ return nil
+}
+
+func (w *HttpWsClientFactory) connect(sessionProvider func() (*Session, error)) (*websocket.Conn, string, Error) {
+ session, err := sessionProvider()
+ if err != nil {
+ return nil, "", SimpleError{code: JmapErrorWssFailedToRetrieveSession, err: err}
+ }
+ if session == nil {
+ return nil, "", SimpleError{code: JmapErrorWssFailedToRetrieveSession, err: nil}
+ }
+ username := session.Username
+ u := session.WebsocketUrl
+
+ h := http.Header{}
+ w.auth(username, h)
+ c, resp, err := w.dialer.Dial(u.String(), h)
+ if err != nil {
+ return nil, "", SimpleError{code: JmapErrorFailedToEstablishWssConnection, err: err}
+ }
+
+ // RFC 8887: Section 4.2:
+ // The reply from the server MUST also contain a corresponding "Sec-WebSocket-Protocol" header
+ // field with a value of "jmap" in order for a JMAP subprotocol connection to be established.
+ if !slices.Contains(resp.Header.Values("Sec-WebSocket-Protocol"), "jmap") {
+ return nil, "", SimpleError{code: JmapErrorWssConnectionResponseMissingJmapSubprotocol}
+ }
+
+ return c, username, nil
+}
+
+type HttpWsClient struct {
+ client *HttpWsClientFactory
+ username string
+ sessionProvider func() (*Session, error)
+ c *websocket.Conn
+ WsClient
+}
+
+func (w *HttpWsClientFactory) EnableNotifications(pushState string, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
+ c, username, jerr := w.connect(sessionProvider)
+ if jerr != nil {
+ return nil, jerr
+ }
+
+ err := c.WriteJSON(WebSocketPushEnable{
+ Type: "WebSocketPushEnable",
+ DataTypes: nil, // = all datatypes
+ PushState: pushState, // will be omitted if empty string
+ })
+ if err != nil {
+ return nil, SimpleError{code: JmapErrorWssFailedToSendWebSocketPushEnable, err: err}
+ }
+
+ return &HttpWsClient{
+ client: w,
+ username: username,
+ sessionProvider: sessionProvider,
+ c: c,
+ }, nil
+}
+
+func (w *HttpWsClientFactory) Close() error {
+ return nil
+}
+
+func (c *HttpWsClient) DisableNotifications() Error {
+ if c.c == nil {
+ return nil
+ }
+
+ err := c.c.WriteJSON(WebSocketPushDisable{
+ Type: "WebSocketPushDisable",
+ })
+ if err != nil {
+ return SimpleError{code: JmapErrorWssFailedToSendWebSocketPushDisable, err: err}
+ }
+
+ err = c.c.Close()
+ if err != nil {
+ return SimpleError{code: JmapErrorWssFailedToClose, err: err}
+ }
+
+ return nil
+}
+
+func (c *HttpWsClient) Close() error {
+ return c.DisableNotifications()
+}
diff --git a/pkg/jmap/jmap_integration_test.go b/pkg/jmap/jmap_integration_test.go
index 5bb9c0dfb5..b46ff851f5 100644
--- a/pkg/jmap/jmap_integration_test.go
+++ b/pkg/jmap/jmap_integration_test.go
@@ -18,6 +18,7 @@ import (
"text/template"
"time"
+ "github.com/gorilla/websocket"
"github.com/jhillyerd/enmime/v2"
"github.com/stretchr/testify/require"
"golang.org/x/text/cases"
@@ -322,6 +323,11 @@ func TestWithStalwart(t *testing.T) {
jh := *http.DefaultClient
jh.Transport = tr
+ wsd := &websocket.Dialer{
+ TLSClientConfig: tlsConfig,
+ HandshakeTimeout: time.Duration(10) * time.Second,
+ }
+
jmapPort, err := container.MappedPort(ctx, httpPort)
require.NoError(err)
jmapBaseUrl := url.URL{
@@ -338,7 +344,10 @@ func TestWithStalwart(t *testing.T) {
nullHttpJmapApiClientEventListener{},
)
- j = NewClient(api, api, api)
+ wscf, err := NewHttpWsClientFactory(wsd, masterUsername, masterPassword, logger)
+ require.NoError(err)
+
+ j = NewClient(api, api, api, wscf)
s, err := j.FetchSession(sessionUrl, username, logger)
require.NoError(err)
// we have to overwrite the hostname in JMAP URL because the container
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index bbbf224a6f..8f9de603fc 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -1182,7 +1182,7 @@ type EmailGetRefCommand struct {
//
// If null, then all records of the data type are returned, if this is supported for that
// data type and the number of records does not exceed the maxObjectsInGet limit.
- IdRef *ResultReference `json:"#ids,omitempty"`
+ IdsRef *ResultReference `json:"#ids,omitempty"`
// The id of the account to use.
AccountId string `json:"accountId"`
@@ -1262,6 +1262,8 @@ type EmailAddress struct {
// SHOULD be used instead. Otherwise, this property is null.
//
// [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
+ //
+ // example: $emailAddressName
Name string `json:"name,omitempty"`
// The addr-spec of the mailbox [RFC5322].
@@ -1275,6 +1277,8 @@ type EmailAddress struct {
//
// [RFC2047]: https://www.rfc-editor.org/rfc/rfc2047.html
// [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
+ //
+ // example: $emailAddressEmail
Email string `json:"email,omitempty"`
}
@@ -1305,36 +1309,42 @@ type EmailHeader struct {
// Email body part.
//
-// The client may specify a partId OR a blobId, but not both.
-// If a partId is given, this partId MUST be present in the bodyValues property.
+// The client may specify a `partId` OR a `blobId`, but not both.
+// If a `partId` is given, this `partId` MUST be present in the `bodyValues` property.
//
-// The charset property MUST be omitted if a partId is given (the part’s content is included
-// in bodyValues, and the server may choose any appropriate encoding).
+// The `charset` property MUST be omitted if a `partId` is given (the part’s content is included
+// in `bodyValues`, and the server may choose any appropriate encoding).
//
-// The size property MUST be omitted if a partId is given. If a blobId is given, it may be
+// The `size` property MUST be omitted if a `partId` is given. If a `blobId` is given, it may be
// included but is ignored by the server (the size is actually calculated from the blob content
// itself).
//
-// A Content-Transfer-Encoding header field MUST NOT be given.
+// A `Content-Transfer-Encoding` header field MUST NOT be given.
type EmailBodyPart struct {
// Identifies this part uniquely within the Email.
//
- // This is scoped to the emailId and has no meaning outside of the JMAP Email object representation.
- // This is null if, and only if, the part is of type multipart/*.
+ // This is scoped to the `emailId` and has no meaning outside of the JMAP Email object representation.
+ // This is null if, and only if, the part is of type `multipart/*`.
+ //
+ // example: $attachmentPartId
PartId string `json:"partId,omitempty"`
// The id representing the raw octets of the contents of the part, after decoding any known
- // Content-Transfer-Encoding (as defined in [RFC2045]), or null if, and only if, the part is of type multipart/*.
+ // `Content-Transfer-Encoding` (as defined in [RFC2045]), or null if, and only if, the part is of type `multipart/*`.
//
// Note that two parts may be transfer-encoded differently but have the same blob id if their decoded octets are identical
// and the server is using a secure hash of the data for the blob id.
// If the transfer encoding is unknown, it is treated as though it had no transfer encoding.
//
// [RFC2045]: https://www.rfc-editor.org/rfc/rfc2045.html
+ //
+ // example: $blobId
BlobId string `json:"blobId,omitempty"`
- // The size, in octets, of the raw data after content transfer decoding (as referenced by the blobId, i.e.,
+ // The size, in octets, of the raw data after content transfer decoding (as referenced by the `blobId`, i.e.,
// the number of octets in the file the user would download).
+ //
+ // example: 31219
Size int `json:"size,omitempty"`
// This is a list of all header fields in the part, in the order they appear in the message.
@@ -1342,64 +1352,81 @@ type EmailBodyPart struct {
// The values are in Raw form.
Headers []EmailHeader `json:"headers,omitempty"`
- // This is the decoded filename parameter of the Content-Disposition header field per [RFC2231], or
- // (for compatibility with existing systems).
- //
- // If not present, then it’s the decoded name parameter of the Content-Type header field per [RFC2047].
+ // This is the decoded filename parameter of the `Content-Disposition` header field per [RFC2231], or
+ // (for compatibility with existing systems) if not present, then it’s the decoded name parameter of
+ // the `Content-Type` header field per [RFC2047].
//
// [RFC2231]: https://www.rfc-editor.org/rfc/rfc2231.html
// [RFC2047]: https://www.rfc-editor.org/rfc/rfc2047.html
+ //
+ // name: $attachmentName
Name string `json:"name,omitempty"`
- // The value of the Content-Type header field of the part, if present; otherwise, the implicit type as per
- // the MIME standard (text/plain or message/rfc822 if inside a multipart/digest).
+ // The value of the `Content-Type` header field of the part, if present; otherwise, the implicit type as per
+ // the MIME standard (`text/plain` or `message/rfc822` if inside a `multipart/digest`).
//
- // CFWS is removed and any parameters are stripped.
+ // [CFWS] is removed and any parameters are stripped.
+ //
+ // [CFWS]: https://www.rfc-editor.org/rfc/rfc5322#section-3.2.2
+ //
+ // example: $attachmentType
Type string `json:"type,omitempty"`
- // The value of the charset parameter of the Content-Type header field, if present, or null if the header
- // field is present but not of type text/*.
+ // The value of the `charset` parameter of the `Content-Type` header field, if present, or null if the header
+ // field is present but not of type `text/*`.
//
- // If there is no Content-Type header field, or it exists and is of type text/* but has no charset parameter,
- // this is the implicit charset as per the MIME standard: us-ascii.
+ // If there is no `Content-Type` header field, or it exists and is of type `text/*` but has no `charset` parameter,
+ // this is the implicit charset as per the MIME standard: `us-ascii`.
+ //
+ // example: $attachmentCharset
Charset string `json:"charset,omitempty"`
- // The value of the Content-Disposition header field of the part, if present;
+ // The value of the `Content-Disposition` header field of the part, if present;
// otherwise, it’s null.
//
- // CFWS is removed and any parameters are stripped.
+ // [CFWS] is removed and any parameters are stripped.
+ //
+ // [CFWS]: https://www.rfc-editor.org/rfc/rfc5322#section-3.2.2
+ //
+ // example: $attachmentDisposition
Disposition string `json:"disposition,omitempty"`
- // The value of the Content-Id header field of the part, if present; otherwise it’s null.
+ // The value of the `Content-Id` header field of the part, if present; otherwise it’s null.
//
- // CFWS and surrounding angle brackets (<>) are removed.
- // This may be used to reference the content from within a text/html body part HTML using the cid: protocol, as defined in [RFC2392].
+ // [CFWS] and surrounding angle brackets (`<>`) are removed.
+ //
+ // This may be used to reference the content from within a `text/html` body part HTML using the `cid:` protocol,
+ // as defined in [RFC2392].
//
// [RFC2392]: https://www.rfc-editor.org/rfc/rfc2392.html
+ // [CFWS]: https://www.rfc-editor.org/rfc/rfc5322#section-3.2.2
+ //
+ // example: $attachmentCid
Cid string `json:"cid,omitempty"`
- // The list of language tags, as defined in [RFC3282], in the Content-Language header field of the part, if present.
+ // The list of language tags, as defined in [RFC3282], in the `Content-Language` header field of the part,
+ // if present.
//
// [RFC3282]: https://www.rfc-editor.org/rfc/rfc3282.html
Language string `json:"language,omitempty"`
- // The URI, as defined in [RFC2557], in the Content-Location header field of the part, if present.
+ // The URI, as defined in [RFC2557], in the `Content-Location` header field of the part, if present.
//
// [RFC2557]: https://www.rfc-editor.org/rfc/rfc2557.html
Location string `json:"location,omitempty"`
- // If the type is multipart/*, this contains the body parts of each child.
+ // If the type is `multipart/*`, this contains the body parts of each child.
SubParts []EmailBodyPart `json:"subParts,omitempty"`
}
type EmailBodyValue struct {
- // The value of the body part after decoding Content-Transfer-Encoding and the Content-Type charset,
+ // The value of the body part after decoding `Content-Transfer-Encoding` and the `Content-Type` charset,
// if both known to the server, and with any CRLF replaced with a single LF.
//
// The server MAY use heuristics to determine the charset to use for decoding if the charset is unknown,
// no charset is given, or it believes the charset given is incorrect.
//
- // Decoding is best effort; the server SHOULD insert the unicode replacement character (U+FFFD) and continue
+ // Decoding is best effort; the server SHOULD insert the unicode replacement character (`U+FFFD`) and continue
// when a malformed section is encountered.
//
// Note that due to the charset decoding and line ending normalisation, the length of this string will
@@ -1424,12 +1451,12 @@ type EmailBodyValue struct {
type Email struct {
// The id of the Email object.
//
- // Note that this is the JMAP object id, NOT the Message-ID header field value of the message [RFC5322].
+ // Note that this is the JMAP object id, NOT the `Message-ID` header field value of the message [RFC5322].
//
// [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
//
// required: true
- // example: eaaaaab
+ // example: $emailId
Id string `json:"id,omitempty"`
// The id representing the raw octets of the message [RFC5322] for this Email.
@@ -1438,12 +1465,12 @@ type Email struct {
//
// [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
//
- // example: cbbrzak0jw3gmtovgtwd1nd1p7p0czjlxx0ejgqws9oucgpuyr9fsayaae
+ // example: $blobId
BlobId string `json:"blobId,omitempty"`
// The id of the Thread to which this Email belongs.
//
- // example: b
+ // example: $threadId
ThreadId string `json:"threadId,omitempty"`
// The set of Mailbox ids this Email belongs to.
@@ -1940,6 +1967,8 @@ type ObjectType string
const (
VacationResponseType ObjectType = "VacationResponse"
EmailType ObjectType = "Email"
+ EmailDeliveryType ObjectType = "EmailDelivery"
+ MailboxType ObjectType = "Mailbox"
)
type Command string
@@ -2756,6 +2785,33 @@ type SearchSnippetGetResponse struct {
NotFound []string `json:"notFound,omitempty"`
}
+type StateChange struct {
+ // This MUST be the string "StateChange".
+ Type string `json:"@type"`
+
+ // A map of an "account id" to an object encoding the state of data types that have
+ // changed for that account since the last StateChange object was pushed, for each
+ // of the accounts to which the user has access and for which something has changed.
+ //
+ // The value is a map. The keys are the type name "Foo" e.g., "Mailbox" or "Email"),
+ // and the value is the "state" property that would currently be returned by a call to
+ // "Foo/get".
+ //
+ // The client can compare the new state strings with its current values to see whether
+ // it has the current data for these types. If not, the changes can then be efficiently
+ // fetched in a single standard API request (using the /changes type methods).
+ Changed map[string]map[ObjectType]string `json:"changed"`
+
+ // A (preferably short) string that encodes the entire server state visible to the user
+ // (not just the objects returned in this call).
+ //
+ // The purpose of the "pushState" token is to allow a client to immediately get any changes
+ // that occurred while it was disconnected. If the server does not support "pushState" tokens,
+ // the client will have to issue a series of "/changes" requests upon reconnection to update
+ // its state to match that of the server.
+ PushState string `json:"pushState"`
+}
+
type ErrorResponse struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
diff --git a/pkg/jmap/jmap_session.go b/pkg/jmap/jmap_session.go
index b2d02bf90d..db20758d59 100644
--- a/pkg/jmap/jmap_session.go
+++ b/pkg/jmap/jmap_session.go
@@ -48,8 +48,9 @@ type Session struct {
// An identifier of the DownloadUrlTemplate to use in metrics and tracing
DownloadEndpoint string
- WebsocketEndpoint *url.URL
+ WebsocketUrl *url.URL
SupportsWebsocketPush bool
+ WebsocketEndpoint string
SessionResponse
}
@@ -91,15 +92,17 @@ func newSession(sessionResponse SessionResponse) (Session, Error) {
}
downloadEndpoint := toEndpoint(downloadUrl)
- var websocketEndpoint *url.URL = nil
+ var websocketUrl *url.URL = nil
+ websocketEndpoint := ""
supportsWebsocketPush := false
- websocketUrl := sessionResponse.Capabilities.Websocket.Url
- if websocketUrl != "" {
- websocketEndpoint, err = url.Parse(websocketUrl)
+ websocketUrlStr := sessionResponse.Capabilities.Websocket.Url
+ if websocketUrlStr != "" {
+ websocketUrl, err = url.Parse(websocketUrlStr)
if err != nil {
return Session{}, invalidSessionResponseErrorInvalidWebsocketUrl
}
supportsWebsocketPush = sessionResponse.Capabilities.Websocket.SupportsPush
+ websocketEndpoint = endpointOf(websocketUrl)
}
return Session{
@@ -110,8 +113,9 @@ func newSession(sessionResponse SessionResponse) (Session, Error) {
UploadEndpoint: uploadEndpoint,
DownloadUrlTemplate: downloadUrl,
DownloadEndpoint: downloadEndpoint,
- WebsocketEndpoint: websocketEndpoint,
+ WebsocketUrl: websocketUrl,
SupportsWebsocketPush: supportsWebsocketPush,
+ WebsocketEndpoint: websocketEndpoint,
SessionResponse: sessionResponse,
}, nil
}
diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go
index c927bbf97a..88ac0f7857 100644
--- a/pkg/jmap/jmap_test.go
+++ b/pkg/jmap/jmap_test.go
@@ -100,6 +100,28 @@ func (h *TestJmapBlobClient) DownloadBinary(ctx context.Context, logger *log.Log
}, nil
}
+func (t TestJmapBlobClient) Close() error {
+ return nil
+}
+
+type TestWsClientFactory struct {
+ WsClientFactory
+}
+
+var _ WsClientFactory = &TestWsClientFactory{}
+
+func NewTestWsClientFactory(t *testing.T) WsClientFactory {
+ return TestWsClientFactory{}
+}
+
+func (t TestWsClientFactory) EnableNotifications(pushState string, sessionProvider func() (*Session, error), listener WsPushListener) (WsClient, Error) {
+ return nil, nil // TODO
+}
+
+func (t TestWsClientFactory) Close() error {
+ return nil
+}
+
func serveTestFile(t *testing.T, name string) ([]byte, Error) {
cwd, _ := os.Getwd()
p := filepath.Join(cwd, "testdata", name)
@@ -147,9 +169,10 @@ func TestRequests(t *testing.T) {
apiClient := NewTestJmapApiClient(t)
wkClient := NewTestJmapWellKnownClient(t)
blobClient := NewTestJmapBlobClient(t)
+ wsClientFactory := NewTestWsClientFactory(t)
logger := log.NopLogger()
ctx := context.Background()
- client := NewClient(wkClient, apiClient, blobClient)
+ client := NewClient(wkClient, apiClient, blobClient, wsClientFactory)
jmapUrl, err := url.Parse("http://localhost/jmap")
require.NoError(err)
diff --git a/services/groupware/api-examples.yaml b/services/groupware/api-examples.yaml
new file mode 100644
index 0000000000..6729b24499
--- /dev/null
+++ b/services/groupware/api-examples.yaml
@@ -0,0 +1,69 @@
+examples:
+ refs:
+ accountId: 'a'
+ emailId: 'bmaaaaa2'
+ threadId: 'b'
+ mailboxIds: {"a": true}
+ emailKeywords: ["$seen", "$notjunk"]
+ emailSize: 3794
+ emailReceivedAt: '2025-09-23T10:58:03Z'
+ emailSentAt: '2025-09-23T12:58:03+02:00'
+ blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf'
+ attachmentName: 'Alloy_Yellow_Scale.pdf'
+ attachmentType: 'application/pdf'
+ attachmentSize: 192128
+ attachmentDisposition: 'attachment'
+ attachmentPartId: '3'
+ attachmentCharset: 'utf-8'
+ attachmentCid: 'c1'
+ emailAddressName: 'Camina Drummer'
+ emailAddressEmail: 'drummer@opa.org'
+ emailSenders:
+ - name: 'Chrisjen Avasarala'
+ email: 'secgen@earth.gov'
+ emailFroms:
+ - name: 'Chrissie'
+ email: 'secgen@earth.gov'
+ emailTos:
+ - name: 'Camina Drummer'
+ email: 'drummer@opa.org'
+ emailCCs:
+ - name: 'Naomi Nagata'
+ email: 'nagata@opa.org'
+ - name: 'James Holden'
+ email: 'holden@earth.gov'
+ emailBCCs:
+ - name: 'Fred Johnson'
+ email: 'johnson@opa.org'
+ emailSubject: 'Food for thought'
+ emailPreview: >-
+ No one starts a war unless I say then can.
+ emailAttachments:
+ - partId: '2'
+ blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf'
+ size: 1374
+ type: 'application/pdf'
+ name: 'the_path.pdf'
+ disposition: 'attachment'
+ - partId: '3'
+ blobId: 'cnz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcq0wqo'
+ charset: 'utf-8'
+ size: 728
+ type: 'text/plain'
+ name: 'secrets.txt'
+ disposition: 'attachment'
+ - partId: '4'
+ blobId: 'caqyey2wobo2bzjkkp2qlsn1ctitl02yylscnb77lc79nvubjihliaiadq'
+ size: 787545
+ name: 'molecule-design.png'
+ type: 'image/png'
+ disposition: 'inline'
+ cid: 'c1'
+ inject:
+ Email:
+ size: $emailSize
+ EmailSummary:
+ size: $emailSize
+ EmailBodyPart:
+ size: $attachmentSize
+
diff --git a/services/groupware/apidoc-process.ts b/services/groupware/apidoc-process.ts
index 809a3875a7..f1a19429a1 100644
--- a/services/groupware/apidoc-process.ts
+++ b/services/groupware/apidoc-process.ts
@@ -1,45 +1,152 @@
import * as fs from 'fs'
import * as yaml from 'js-yaml'
+const API_PARAMS_CONFIG_FILE = 'api-params.yaml'
+const API_EXAMPLES_CONFIG_FILE = 'api-examples.yaml'
+
+interface Response {
+ $ref: string
+}
+
+interface Parameter {
+ type: string
+ required: boolean
+ format: string
+ example: any
+ name: string
+ description: string
+ in: string
+}
+
+interface VerbData {
+ tags: string[]
+ summary: string
+ description: string | undefined
+ operationId: string
+ parameters: Parameter[]
+ responses: {[status:string]:Response}
+}
+
+interface Item {
+ $ref: string
+}
+
+interface AdditionalProperties {
+ $ref: string
+}
+
+interface Property {
+ description: string
+ type: string
+ items: Item
+ example: any
+ additionalProperties: AdditionalProperties
+}
+
+interface Definition {
+ type: string
+ title: string
+ required: string[]
+ properties: {[property:string]:Property}
+}
+
+interface OpenApi {
+ paths: {[path:string]:{[verb:string]:VerbData}}
+ definitions: {[type:string]:Definition}
+}
+
interface Param {
description: string
type: string
}
-interface Config {
+interface ParamsConfig {
params: {[param:string]:Param}
}
+interface ExamplesConfigExamples {
+ refs: {[id:string]:any}
+ inject: {[id:string]:{[property:string]:any}}
+}
+
+interface ExamplesConfig {
+ examples: ExamplesConfigExamples
+}
+
let inputData = ''
process.stdin.on('data', (chunk) => {
inputData += chunk.toString()
})
+const usedExamples = new Set()
+const unresolvedExampleReferences = new Set()
+
process.stdin.on('end', () => {
try {
- const config = yaml.load(fs.readFileSync('api-params.yaml', 'utf8')) as Config
- const params = config.params || {}
+ const paramsConfig = yaml.load(fs.readFileSync(API_PARAMS_CONFIG_FILE, 'utf8')) as ParamsConfig
+ const params = paramsConfig.params || {}
- const data = yaml.load(inputData) as any
+ const examplesConfig = yaml.load(fs.readFileSync(API_EXAMPLES_CONFIG_FILE, 'utf8')) as ExamplesConfig
+ const exampleRefs = examplesConfig.examples.refs
+ const exampleInjects = examplesConfig.examples.inject
+
+ const data = yaml.load(inputData) as OpenApi
for (const path in data.paths) {
+ const pathData = data.paths[path]
+
for (const param in params) {
if (path.includes(`{${param}}`)) {
const paramsData = params[param] as Param
- const pathData = data.paths[path] as any
for (const verb in pathData) {
- const verbData = pathData[verb] as any
- if (!Object.getOwnPropertyNames(verbData).includes('parameters')) {
- verbData.parameters = []
- }
- verbData['parameters'].push({
+ const verbData = pathData[verb]
+ verbData.parameters ??= []
+ verbData.parameters.push({
name: param,
required: true,
- type: paramsData.type !== 'undefined' ? paramsData.type : 'string',
+ type: paramsData.type !== undefined ? paramsData.type : 'string',
in: 'path',
description: paramsData.description,
- })
+ } as Parameter)
+ }
+ }
+ }
+
+ // do some magic with the formatting of endpoint descriptions:
+ for (const verb in pathData) {
+ const verbData = pathData[verb]
+ if (verbData.description !== null && verbData.description !== undefined) {
+ verbData.description = verbData.description.split("\n").map((line) => {
+ return line.replace(/^(\s*)!/, '$1*')
+ }).join("\n")
+ }
+ }
+ }
+
+ for (const def in data.definitions) {
+ const defData = data.definitions[def]
+ if (defData.properties !== null && defData.properties !== undefined) {
+ const injects = exampleInjects[def] || {}
+ for (const prop in defData.properties as any) {
+ const propData = defData.properties[prop]
+
+ const inject = injects[prop]
+ if (inject !== null && inject !== undefined) {
+ propData.example = inject
+ }
+
+ if (propData.example !== null && propData.example !== undefined) {
+ if (typeof propData.example === 'string' && (propData.example as string).startsWith('$')) {
+ const exampleId = propData.example.substring(1)
+ const value = exampleRefs[exampleId]
+ if (value === null || value === undefined) {
+ unresolvedExampleReferences.add(exampleId)
+ } else {
+ usedExamples.add(exampleId)
+ propData.example = value
+ }
+ }
}
}
}
@@ -47,6 +154,28 @@ process.stdin.on('end', () => {
process.stdout.write(yaml.dump(data))
process.stdout.write("\n")
+
+ if (unresolvedExampleReferences.size > 0) {
+ console.error(`\x1b[33;1m⚠️ WARNING: unresolved example references not contained in ${API_PARAMS_CONFIG_FILE}:\x1b[0m`)
+ unresolvedExampleReferences.forEach(item => {
+ console.error(` - ${item}`)
+ })
+ console.error()
+ }
+
+ const unusedExampleReferences = new Set(Object.keys(exampleRefs))
+ usedExamples.forEach(item => {
+ unusedExampleReferences.delete(item)
+ })
+
+ if (unusedExampleReferences.size > 0) {
+ console.error(`\x1b[33;1m⚠️ WARNING: unused examples in ${API_EXAMPLES_CONFIG_FILE}:\x1b[0m`)
+ unusedExampleReferences.forEach(item => {
+ console.error(` - ${item}`)
+ })
+ console.error()
+ }
+
} catch (error) {
if (error instanceof Error) {
console.error(`Error occured while post-processing OpenAPI: ${error.message}`)
diff --git a/services/groupware/package.json b/services/groupware/package.json
index 085b7ba123..749d280df4 100644
--- a/services/groupware/package.json
+++ b/services/groupware/package.json
@@ -1,13 +1,13 @@
{
"dependencies": {
- "@redocly/cli": "^2.0.8",
+ "@redocly/cli": "^2.2.0",
"@types/js-yaml": "^4.0.9",
"cheerio": "^1.1.2",
"js-yaml": "^4.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
},
- "packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
+ "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a",
"type": "module",
"devDependencies": {
"@types/node": "^24.3.1"
diff --git a/services/groupware/pkg/config/config.go b/services/groupware/pkg/config/config.go
index bcd75e8a4d..60dcdc2e2b 100644
--- a/services/groupware/pkg/config/config.go
+++ b/services/groupware/pkg/config/config.go
@@ -44,5 +44,6 @@ type Mail struct {
DefaultEmailLimit uint `yaml:"default_email_limit" env:"GROUPWARE_DEFAULT_EMAIL_LIMIT"`
MaxBodyValueBytes uint `yaml:"max_body_value_bytes" env:"GROUPWARE_MAX_BODY_VALUE_BYTES"`
ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout" env:"GROUPWARE_RESPONSE_HEADER_TIMEOUT"`
+ PushHandshakeTimeout time.Duration `yaml:"push_handshake_timeout" env:"GROUPWARE_PUSH_HANDSHAKE_TIMEOUT"`
SessionCache MailSessionCache `yaml:"session_cache"`
}
diff --git a/services/groupware/pkg/config/defaults/defaultconfig.go b/services/groupware/pkg/config/defaults/defaultconfig.go
index d40f26eac0..a5b8f444ba 100644
--- a/services/groupware/pkg/config/defaults/defaultconfig.go
+++ b/services/groupware/pkg/config/defaults/defaultconfig.go
@@ -34,6 +34,7 @@ func DefaultConfig() *config.Config {
DefaultEmailLimit: uint(0),
MaxBodyValueBytes: uint(0),
ResponseHeaderTimeout: 10 * time.Second,
+ PushHandshakeTimeout: 10 * time.Second,
SessionCache: config.MailSessionCache{
Ttl: 5 * time.Minute,
FailureTtl: 15 * time.Second,
diff --git a/services/groupware/pkg/groupware/groupware_api_messages.go b/services/groupware/pkg/groupware/groupware_api_emails.go
similarity index 67%
rename from services/groupware/pkg/groupware/groupware_api_messages.go
rename to services/groupware/pkg/groupware/groupware_api_emails.go
index 9ecdb09ff0..f676e717e3 100644
--- a/services/groupware/pkg/groupware/groupware_api_messages.go
+++ b/services/groupware/pkg/groupware/groupware_api_emails.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
+ "sort"
"strconv"
"strings"
"time"
@@ -14,6 +15,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/log"
+ "github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics"
)
@@ -828,6 +830,321 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
})
}
+type EmailSummary struct {
+ // The id of the account this Email summary pertains to.
+ // required: true
+ // example: $accountId
+ AccountId string `json:"accountId,omitempty"`
+
+ // The id of the Email object.
+ //
+ // Note that this is the JMAP object id, NOT the Message-ID header field value of the message [RFC5322].
+ //
+ // [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
+ //
+ // required: true
+ // example: $emailId
+ Id string `json:"id,omitempty"`
+
+ // The id of the Thread to which this Email belongs.
+ //
+ // example: $threadId
+ ThreadId string `json:"threadId,omitempty"`
+
+ // The set of Mailbox ids this Email belongs to.
+ //
+ // An Email in the mail store MUST belong to one or more Mailboxes at all times (until it is destroyed).
+ // The set is represented as an object, with each key being a Mailbox id.
+ //
+ // The value for each key in the object MUST be true.
+ //
+ // example: $mailboxIds
+ MailboxIds map[string]bool `json:"mailboxIds,omitempty"`
+
+ // A set of keywords that apply to the Email.
+ //
+ // The set is represented as an object, with the keys being the keywords.
+ //
+ // The value for each key in the object MUST be true.
+ //
+ // Keywords are shared with IMAP.
+ //
+ // The six system keywords from IMAP get special treatment.
+ //
+ // The following four keywords have their first character changed from \ in IMAP to $ in JMAP and have particular semantic meaning:
+ //
+ // - $draft: The Email is a draft the user is composing.
+ // - $seen: The Email has been read.
+ // - $flagged: The Email has been flagged for urgent/special attention.
+ // - $answered: The Email has been replied to.
+ //
+ // The IMAP \Recent keyword is not exposed via JMAP. The IMAP \Deleted keyword is also not present: IMAP uses a delete+expunge model,
+ // which JMAP does not. Any message with the \Deleted keyword MUST NOT be visible via JMAP (and so are not counted in the
+ // “totalEmails”, “unreadEmails”, “totalThreads”, and “unreadThreads” Mailbox properties).
+ //
+ // Users may add arbitrary keywords to an Email.
+ // For compatibility with IMAP, a keyword is a case-insensitive string of 1–255 characters in the ASCII subset
+ // %x21–%x7e (excludes control chars and space), and it MUST NOT include any of these characters:
+ //
+ // ( ) { ] % * " \
+ //
+ // Because JSON is case sensitive, servers MUST return keywords in lowercase.
+ //
+ // The [IMAP and JMAP Keywords] registry as established in [RFC5788] assigns semantic meaning to some other
+ // keywords in common use.
+ //
+ // New keywords may be established here in the future. In particular, note:
+ //
+ // - $forwarded: The Email has been forwarded.
+ // - $phishing: The Email is highly likely to be phishing.
+ // Clients SHOULD warn users to take care when viewing this Email and disable links and attachments.
+ // - $junk: The Email is definitely spam.
+ // Clients SHOULD set this flag when users report spam to help train automated spam-detection systems.
+ // - $notjunk: The Email is definitely not spam.
+ // Clients SHOULD set this flag when users indicate an Email is legitimate, to help train automated spam-detection systems.
+ //
+ // [IMAP and JMAP Keywords]: https://www.iana.org/assignments/imap-jmap-keywords/
+ // [RFC5788]: https://www.rfc-editor.org/rfc/rfc5788.html
+ //
+ // example: $emailKeywords
+ Keywords map[string]bool `json:"keywords,omitempty"`
+
+ // The size, in octets, of the raw data for the message [RFC5322]
+ // (as referenced by the blobId, i.e., the number of octets in the file the user would download).
+ //
+ // [RFC5322]: https://www.rfc-editor.org/rfc/rfc5322.html
+ Size int `json:"size"`
+
+ // The date the Email was received by the message store.
+ //
+ // This is the internal date in IMAP [RFC3501].
+ //
+ // [RFC3501]: https://www.rfc-editor.org/rfc/rfc3501.html
+ //
+ // example: $emailReceivedAt
+ ReceivedAt time.Time `json:"receivedAt,omitzero"`
+
+ // The value is identical to the value of header:Sender:asAddresses.
+ // example: $emailSenders
+ Sender []jmap.EmailAddress `json:"sender,omitempty"`
+
+ // The value is identical to the value of header:From:asAddresses.
+ // example: $emailFroms
+ From []jmap.EmailAddress `json:"from,omitempty"`
+
+ // The value is identical to the value of header:To:asAddresses.
+ // example: $emailTos
+ To []jmap.EmailAddress `json:"to,omitempty"`
+
+ // The value is identical to the value of header:Cc:asAddresses.
+ // example: $emailCCs
+ Cc []jmap.EmailAddress `json:"cc,omitempty"`
+
+ // The value is identical to the value of header:Bcc:asAddresses.
+ // example: $emailBCCs
+ Bcc []jmap.EmailAddress `json:"bcc,omitempty"`
+
+ // The value is identical to the value of header:Subject:asText.
+ // example: $emailSubject
+ Subject string `json:"subject,omitempty"`
+
+ // The value is identical to the value of header:Date:asDate.
+ // example: $emailSentAt
+ SentAt time.Time `json:"sentAt,omitzero"`
+
+ // This is true if there are one or more parts in the message that a client UI should offer as downloadable.
+ //
+ // A server SHOULD set hasAttachment to true if the attachments list contains at least one item that
+ // does not have Content-Disposition: inline.
+ //
+ // The server MAY ignore parts in this list that are processed automatically in some way or are referenced
+ // as embedded images in one of the text/html parts of the message.
+ //
+ // The server MAY set hasAttachment based on implementation-defined or site-configurable heuristics.
+ // example: true
+ HasAttachment bool `json:"hasAttachment,omitempty"`
+
+ // A list, traversing depth-first, of all parts in bodyStructure.
+ //
+ // They must satisfy either of the following conditions:
+ //
+ // - not of type multipart/* and not included in textBody or htmlBody
+ // - of type image/*, audio/*, or video/* and not in both textBody and htmlBody
+ //
+ // None of these parts include subParts, including message/* types.
+ //
+ // Attached messages may be fetched using the Email/parse method and the blobId.
+ //
+ // Note that a text/html body part HTML may reference image parts in attachments by using cid:
+ // links to reference the Content-Id, as defined in [RFC2392], or by referencing the Content-Location.
+ //
+ // [RFC2392]: https://www.rfc-editor.org/rfc/rfc2392.html
+ //
+ // example: $emailAttachments
+ Attachments []jmap.EmailBodyPart `json:"attachments,omitempty"`
+
+ // A plaintext fragment of the message body.
+ //
+ // This is intended to be shown as a preview line when listing messages in the mail store and may be truncated
+ // when shown.
+ //
+ // The server may choose which part of the message to include in the preview; skipping quoted sections and
+ // salutations and collapsing white space can result in a more useful preview.
+ //
+ // This MUST NOT be more than 256 characters in length.
+ //
+ // As this is derived from the message content by the server, and the algorithm for doing so could change over
+ // time, fetching this for an Email a second time MAY return a different result.
+ // However, the previous value is not considered incorrect, and the change SHOULD NOT cause the Email object
+ // to be considered as changed by the server.
+ //
+ // example: $emailPreview
+ Preview string `json:"preview,omitempty"`
+}
+
+func summarizeEmail(accountId string, email jmap.Email) EmailSummary {
+ return EmailSummary{
+ AccountId: accountId,
+ Id: email.Id,
+ ThreadId: email.ThreadId,
+ MailboxIds: email.MailboxIds,
+ Keywords: email.Keywords,
+ Size: email.Size,
+ ReceivedAt: email.ReceivedAt,
+ Sender: email.Sender,
+ From: email.From,
+ To: email.To,
+ Cc: email.Cc,
+ Bcc: email.Bcc,
+ Subject: email.Subject,
+ SentAt: email.SentAt,
+ HasAttachment: email.HasAttachment,
+ Attachments: email.Attachments,
+ Preview: email.Preview,
+ }
+}
+
+type emailWithAccountId struct {
+ accountId string
+ email jmap.Email
+}
+
+// When the request succeeds.
+// swagger:response GetLatestEmailsSummaryForAllAccounts200
+type SwaggerGetLatestEmailsSummaryForAllAccounts200 struct {
+ // in: body
+ Body []EmailSummary
+}
+
+// swagger:parameters get_latest_emails_summary_for_all_accounts
+type SwaggerGetLatestEmailsSummaryForAllAccountsParams struct {
+ // in: query
+ // example: 10
+ Limit uint `json:"limit"`
+
+ // in: query
+ // example: true
+ Unread bool `json:"unread"`
+
+ // in: query
+ // example: false
+ Undesirable bool `json:"undesirable"`
+}
+
+// swagger:route GET /groupware/accounts/all/emails/latest/summary email get_latest_emails_summary_for_all_accounts
+// Get a summary of the latest emails across all the mailboxes, across all of a user's accounts.
+//
+// Retrieves summaries of the latest emails of a user, in all accounts, across all mailboxes.
+//
+// The number of total summaries to retrieve is specified using the query parameter 'limit'.
+//
+// The following additional query parameters may be specified to further filter the emails to summarize:
+//
+// ! `unread`: when `true`, only unread emails will be summarized (default is to summarize all emails, read or unread)
+// ! `undesirable`: when `true`, emails that are flagged as spam or phishing will also be summarized (default is to ignore those)
+//
+// responses:
+//
+// 200: GetLatestEmailsSummaryForAllAccounts200
+// 400: ErrorResponse400
+// 404: ErrorResponse404
+// 500: ErrorResponse500
+func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ l := req.logger.With()
+ limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO from configuration
+ if err != nil {
+ return errorResponse(err)
+ }
+ if ok {
+ l = l.Uint(QueryParamLimit, limit)
+ }
+
+ unread, ok, err := req.parseBoolParam(QueryParamUnread, false)
+ if err != nil {
+ return errorResponse(err)
+ }
+ if ok {
+ l = l.Bool(QueryParamUnread, unread)
+ }
+
+ undesirable, ok, err := req.parseBoolParam(QueryParamUndesirable, false)
+ if err != nil {
+ return errorResponse(err)
+ }
+ if ok {
+ l = l.Bool(QueryParamUndesirable, undesirable)
+ }
+
+ var filter jmap.EmailFilterElement = nil // all emails, read and unread
+ {
+ notKeywords := []string{}
+ if unread {
+ notKeywords = append(notKeywords, jmap.JmapKeywordSeen)
+ }
+ if undesirable {
+ notKeywords = append(notKeywords, jmap.JmapKeywordJunk, jmap.JmapKeywordPhishing)
+ }
+ filter = filterFromNotKeywords(notKeywords)
+ }
+
+ allAccountIds := structs.Keys(req.session.Accounts) // TODO(pbleser-oc) do we need a limit for a maximum amount of accounts to query at once?
+ l.Array(logAccountId, log.SafeStringArray(allAccountIds))
+
+ logger := log.From(l)
+
+ emailsSummariesByAccount, sessionState, jerr := g.jmap.QueryEmailSummaries(allAccountIds, req.session, req.ctx, logger, filter, limit)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ // sort in memory to respect the overall limit
+ total := uint(0)
+ for _, emails := range emailsSummariesByAccount {
+ total += uint(max(len(emails.Emails), 0))
+ }
+ all := make([]emailWithAccountId, total)
+ i := uint(0)
+ for accountId, emails := range emailsSummariesByAccount {
+ for _, email := range emails.Emails {
+ all[i] = emailWithAccountId{accountId: accountId, email: email}
+ i++
+ }
+ }
+
+ sort.Slice(all, func(i int, j int) bool {
+ return all[i].email.ReceivedAt.Before(all[j].email.ReceivedAt)
+ })
+
+ summaries := make([]EmailSummary, min(limit, total))
+ for i = 0; i < limit && i < total; i++ {
+ summaries[i] = summarizeEmail(all[i].accountId, all[i].email)
+ }
+
+ return response(summaries, sessionState)
+ })
+}
+
func filterEmails(all []jmap.Email, skip jmap.Email) []jmap.Email {
filtered := all[:0]
for _, email := range all {
@@ -837,3 +1154,18 @@ func filterEmails(all []jmap.Email, skip jmap.Email) []jmap.Email {
}
return filtered
}
+
+func filterFromNotKeywords(keywords []string) jmap.EmailFilterElement {
+ switch len(keywords) {
+ case 0:
+ return nil
+ case 1:
+ return jmap.EmailFilterCondition{NotKeyword: keywords[0]}
+ default:
+ conditions := make([]jmap.EmailFilterElement, len(keywords))
+ for i, keyword := range keywords {
+ conditions[i] = jmap.EmailFilterCondition{NotKeyword: keyword}
+ }
+ return jmap.EmailFilterOperator{Operator: jmap.And, Conditions: conditions}
+ }
+}
diff --git a/services/groupware/pkg/groupware/groupware_framework.go b/services/groupware/pkg/groupware/groupware_framework.go
index 0293408599..68ec68929c 100644
--- a/services/groupware/pkg/groupware/groupware_framework.go
+++ b/services/groupware/pkg/groupware/groupware_framework.go
@@ -12,6 +12,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
+ "github.com/gorilla/websocket"
"github.com/miekg/dns"
"github.com/r3labs/sse/v2"
"github.com/rs/zerolog"
@@ -177,6 +178,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
sessionCacheMaxCapacity := uint64(max(config.Mail.SessionCache.MaxCapacity, 0))
sessionCacheTtl := max(config.Mail.SessionCache.Ttl, 0)
sessionFailureCacheTtl := max(config.Mail.SessionCache.FailureTtl, 0)
+ wsHandshakeTimeout := config.Mail.PushHandshakeTimeout
eventChannelSize := 100 // TODO make channel queue buffering size configurable
workerQueueSize := 100 // TODO configuration setting
@@ -195,7 +197,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.ResponseHeaderTimeout = responseHeaderTimeout
if insecureTls {
- tlsConfig := &tls.Config{InsecureSkipVerify: true} // TODO make configurable
+ tlsConfig := &tls.Config{InsecureSkipVerify: true}
httpTransport.TLSClientConfig = tlsConfig
}
httpClient := *http.DefaultClient
@@ -212,8 +214,21 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
jmapMetricsAdapter,
)
+ wsDialer := &websocket.Dialer{
+ HandshakeTimeout: wsHandshakeTimeout,
+ }
+ if insecureTls {
+ wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+ }
+
+ wsf, err := jmap.NewHttpWsClientFactory(wsDialer, masterUsername, masterPassword, logger)
+ if err != nil {
+ logger.Error().Err(err).Msg("failed to create websocket client")
+ return nil, GroupwareInitializationError{Message: "failed to create websocket client", Err: err}
+ }
+
// api implements all three interfaces:
- jmapClient := jmap.NewClient(api, api, api)
+ jmapClient := jmap.NewClient(api, api, api, wsf)
sessionCacheBuilder := newSessionCacheBuilder(
sessionUrl,
@@ -426,7 +441,7 @@ func (g *Groupware) ServeSSE(w http.ResponseWriter, r *http.Request) {
}
// Provide a JMAP Session for the
-func (g *Groupware) session(user user, _ *http.Request, _ context.Context, _ *log.Logger) (jmap.Session, bool, *GroupwareError) {
+func (g *Groupware) session(user user, _ *log.Logger) (jmap.Session, bool, *GroupwareError) {
s := g.sessionCache.Get(user.GetUsername())
if s != nil {
if s.Success() {
@@ -482,6 +497,23 @@ func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Er
render.Render(w, r, errorResponses(*error))
}
+func (g *Groupware) getSession(user user) (*jmap.Session, *GroupwareError) {
+ session, ok, gwerr := g.session(user, g.logger)
+ if gwerr != nil {
+ g.metrics.SessionFailureCounter.Inc()
+ g.logger.Error().Str("code", gwerr.Code).Str("error", gwerr.Title).Str("detail", gwerr.Detail).Msg("failed to determine JMAP session")
+ return nil, gwerr
+ }
+ if !ok {
+ // no session = authentication failed
+ gwerr = &ErrorInvalidAuthentication
+ g.metrics.SessionFailureCounter.Inc()
+ g.logger.Error().Msg("could not authenticate, failed to find Session")
+ return nil, gwerr
+ }
+ return &session, nil
+}
+
func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler func(r Request) Response) (Response, bool) {
ctx := r.Context()
sl := g.logger.SubloggerWithRequestID(ctx)
@@ -501,7 +533,7 @@ func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler
logger = log.From(logger.With().Str(logUserId, log.SafeString(user.GetId())))
- session, ok, gwerr := g.session(user, r, ctx, logger)
+ session, ok, gwerr := g.session(user, logger)
if gwerr != nil {
g.metrics.SessionFailureCounter.Inc()
errorId := errorId(r, ctx)
@@ -606,7 +638,7 @@ func (g *Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(
logger = log.From(logger.With().Str(logUserId, log.SafeString(user.GetId())))
- session, ok, gwerr := g.session(user, r, ctx, logger)
+ session, ok, gwerr := g.session(user, logger)
if gwerr != nil {
errorId := errorId(r, ctx)
logger.Error().Str("code", gwerr.Code).Str("error", gwerr.Title).Str("detail", gwerr.Detail).Str(logErrorId, errorId).Msg("failed to determine JMAP session")
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index d7988bfb96..8f4ac26174 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -44,6 +44,8 @@ const (
QueryParamPartId = "partId"
QueryParamAttachmentName = "name"
QueryParamAttachmentBlobId = "blobId"
+ QueryParamUnread = "unread"
+ QueryParamUndesirable = "undesirable"
HeaderSince = "if-none-match"
)
@@ -57,6 +59,9 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/roles", g.GetMailboxRoles) // ?role=
r.Get("/roles/{role}", g.GetMailboxByRoleForAllAccounts) // ?role=
})
+ r.Route("/emails", func(r chi.Router) {
+ r.Get("/latest/summary", g.GetLatestEmailsSummaryForAllAccounts)
+ })
})
r.Route("/accounts/{accountid}", func(r chi.Router) {
r.Get("/", g.GetAccount)
diff --git a/services/groupware/pnpm-lock.yaml b/services/groupware/pnpm-lock.yaml
index 7b1fc93388..389a3eb46a 100644
--- a/services/groupware/pnpm-lock.yaml
+++ b/services/groupware/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@redocly/cli':
- specifier: ^2.0.8
- version: 2.0.8(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)
+ specifier: ^2.2.0
+ version: 2.2.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)
'@types/js-yaml':
specifier: ^4.0.9
version: 4.0.9
@@ -45,6 +45,10 @@ packages:
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
engines: {node: '>=6.9.0'}
+ '@babel/runtime@7.28.4':
+ resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+ engines: {node: '>=6.9.0'}
+
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -219,27 +223,27 @@ packages:
'@redocly/ajv@8.11.3':
resolution: {integrity: sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==}
- '@redocly/cli@2.0.8':
- resolution: {integrity: sha512-FWBhB2hvF8rXWViCVgbKT3AmQH9ChRDNCmN3SyLRaL0GQ5SLi10p9rV26/7GktcMAKetM9XC4ETQQJMup3CVQQ==}
+ '@redocly/cli@2.2.0':
+ resolution: {integrity: sha512-3kXAcA7JLElZH206XqOiFcYtNlIhJeGFsopRypygO3W36sRvFO1d824jZeboYq9ROUaqij7nBCqBoBRKtldtDQ==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
hasBin: true
'@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
- '@redocly/config@0.29.0':
- resolution: {integrity: sha512-AkP1Berx9GvD15aN6r0IcOo289ElHp52XgeFTxXCumJ4gaUXUmvzqfZTfFFJWDaGgZvUZvmQrs1UdaPjZEXeHA==}
+ '@redocly/config@0.31.0':
+ resolution: {integrity: sha512-KPm2v//zj7qdGvClX0YqRNLQ9K7loVJWFEIceNxJIYPXP4hrhNvOLwjmxIkdkai0SdqYqogR2yjM/MjF9/AGdQ==}
'@redocly/openapi-core@1.34.5':
resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
- '@redocly/openapi-core@2.0.8':
- resolution: {integrity: sha512-ShnpeEgcQY0YxopFDH/m94PWiHgCuWGa9FIcHchdHMZGA0mgV/Eojhi46A/hi2KP7TaVsgZc6+8u7GxTTeTC5g==}
+ '@redocly/openapi-core@2.2.0':
+ resolution: {integrity: sha512-gHedIv/1V5l7x1Nkb/kzRr8rlbGSqmDRs2AabO6BbV3cDofIwkRU7BJRFFNtjTq5LuLIWRTDYghfuIAG94/1Iw==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
- '@redocly/respect-core@2.0.8':
- resolution: {integrity: sha512-gKOjUn/UmoHYIDKHV7RanAvFkhQIgQh9oiA3qfYHMI/N4+tWGoJd03Soc+2bn3Zie8DTXjKwQIy0k5n2kZA7hw==}
+ '@redocly/respect-core@2.2.0':
+ resolution: {integrity: sha512-agpspN6zZWSjSrpdtAKiT8vVMNSs/T0pKRZfgj3ZSvB/asfenJDdDRhDc0h0w2LTYlbcsHEn+TWj4d5czvwlWg==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
'@sinclair/typebox@0.27.8':
@@ -1222,6 +1226,8 @@ snapshots:
'@babel/runtime@7.28.3': {}
+ '@babel/runtime@7.28.4': {}
+
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -1391,14 +1397,14 @@ snapshots:
require-from-string: 2.0.2
uri-js-replace: 1.0.1
- '@redocly/cli@2.0.8(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)':
+ '@redocly/cli@2.2.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)':
dependencies:
'@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
- '@redocly/openapi-core': 2.0.8(ajv@8.17.1)
- '@redocly/respect-core': 2.0.8(ajv@8.17.1)
+ '@redocly/openapi-core': 2.2.0(ajv@8.17.1)
+ '@redocly/respect-core': 2.2.0(ajv@8.17.1)
abort-controller: 3.0.0
chokidar: 3.6.0
colorette: 1.4.0
@@ -1431,7 +1437,7 @@ snapshots:
'@redocly/config@0.22.2': {}
- '@redocly/config@0.29.0':
+ '@redocly/config@0.31.0':
dependencies:
json-schema-to-ts: 2.7.2
@@ -1449,10 +1455,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@redocly/openapi-core@2.0.8(ajv@8.17.1)':
+ '@redocly/openapi-core@2.2.0(ajv@8.17.1)':
dependencies:
'@redocly/ajv': 8.11.3
- '@redocly/config': 0.29.0
+ '@redocly/config': 0.31.0
ajv-formats: 2.1.1(ajv@8.17.1)
colorette: 1.4.0
js-levenshtein: 1.1.6
@@ -1463,15 +1469,14 @@ snapshots:
transitivePeerDependencies:
- ajv
- '@redocly/respect-core@2.0.8(ajv@8.17.1)':
+ '@redocly/respect-core@2.2.0(ajv@8.17.1)':
dependencies:
'@faker-js/faker': 7.6.0
'@noble/hashes': 1.8.0
'@redocly/ajv': 8.11.2
- '@redocly/openapi-core': 2.0.8(ajv@8.17.1)
+ '@redocly/openapi-core': 2.2.0(ajv@8.17.1)
better-ajv-errors: 1.2.0(ajv@8.17.1)
colorette: 2.0.20
- jest-diff: 29.7.0
jest-matcher-utils: 29.7.0
json-pointer: 0.6.2
jsonpath-plus: 10.3.0
@@ -1921,7 +1926,7 @@ snapshots:
json-schema-to-ts@2.7.2:
dependencies:
- '@babel/runtime': 7.28.3
+ '@babel/runtime': 7.28.4
'@types/json-schema': 7.0.15
ts-algebra: 1.2.2
diff --git a/services/groupware/pnpm-workspace.yaml b/services/groupware/pnpm-workspace.yaml
new file mode 100644
index 0000000000..54168a08ef
--- /dev/null
+++ b/services/groupware/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+onlyBuiltDependencies:
+ - core-js
+ - protobufjs
diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore
new file mode 100644
index 0000000000..cd3fcd1ef7
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/.gitignore
@@ -0,0 +1,25 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+
+.idea/
+*.iml
diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS
new file mode 100644
index 0000000000..1931f40068
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/AUTHORS
@@ -0,0 +1,9 @@
+# This is the official list of Gorilla WebSocket authors for copyright
+# purposes.
+#
+# Please keep the list sorted.
+
+Gary Burd
+Google LLC (https://opensource.google.com/)
+Joachim Bauch
+
diff --git a/vendor/github.com/gorilla/websocket/LICENSE b/vendor/github.com/gorilla/websocket/LICENSE
new file mode 100644
index 0000000000..9171c97225
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
+
+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.
+
+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 HOLDER 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.
diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md
new file mode 100644
index 0000000000..d33ed7fdd8
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/README.md
@@ -0,0 +1,33 @@
+# Gorilla WebSocket
+
+[](https://godoc.org/github.com/gorilla/websocket)
+[](https://circleci.com/gh/gorilla/websocket)
+
+Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
+[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
+
+
+### Documentation
+
+* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
+* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
+* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
+* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
+* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
+
+### Status
+
+The Gorilla WebSocket package provides a complete and tested implementation of
+the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
+package API is stable.
+
+### Installation
+
+ go get github.com/gorilla/websocket
+
+### Protocol Compliance
+
+The Gorilla WebSocket package passes the server tests in the [Autobahn Test
+Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
+subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
+
diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go
new file mode 100644
index 0000000000..04fdafee18
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/client.go
@@ -0,0 +1,434 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/http/httptrace"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// ErrBadHandshake is returned when the server response to opening handshake is
+// 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
+// (Cookie). Use the response.Header to get the selected subprotocol
+// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
+//
+// 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) {
+ d := Dialer{
+ ReadBufferSize: readBufSize,
+ WriteBufferSize: writeBufSize,
+ NetDial: func(net, addr string) (net.Conn, error) {
+ return netConn, nil
+ },
+ }
+ return d.Dial(u.String(), requestHeader)
+}
+
+// A Dialer contains options for connecting to WebSocket server.
+//
+// It is safe to call Dialer's methods concurrently.
+type Dialer struct {
+ // NetDial specifies the dial function for creating TCP connections. If
+ // NetDial is nil, net.Dial is used.
+ NetDial func(network, addr string) (net.Conn, error)
+
+ // NetDialContext specifies the dial function for creating TCP connections. If
+ // NetDialContext is nil, NetDial is used.
+ NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
+
+ // NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If
+ // NetDialTLSContext is nil, NetDialContext is used.
+ // If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and
+ // TLSClientConfig is ignored.
+ NetDialTLSContext func(ctx context.Context, 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.
+ // If either NetDialTLS or NetDialTLSContext are set, Dial assumes the TLS handshake
+ // is done there and TLSClientConfig is ignored.
+ TLSClientConfig *tls.Config
+
+ // HandshakeTimeout specifies the duration for the handshake to complete.
+ HandshakeTimeout time.Duration
+
+ // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. 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
+
+ // WriteBufferPool is a pool of buffers for write operations. If the value
+ // is not set, then write buffers are allocated to the connection for the
+ // lifetime of the connection.
+ //
+ // A pool is most useful when the application has a modest volume of writes
+ // across a large number of connections.
+ //
+ // Applications should use a single pool for each unique value of
+ // WriteBufferSize.
+ WriteBufferPool BufferPool
+
+ // Subprotocols specifies the client's requested subprotocols.
+ Subprotocols []string
+
+ // 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
+
+ // Jar specifies the cookie jar.
+ // If Jar is nil, cookies are not sent in requests and ignored
+ // in responses.
+ Jar http.CookieJar
+}
+
+// Dial creates a new client connection by calling DialContext with a background context.
+func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
+ return d.DialContext(context.Background(), urlStr, requestHeader)
+}
+
+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 {
+ switch u.Scheme {
+ case "wss":
+ hostPort += ":443"
+ case "https":
+ hostPort += ":443"
+ default:
+ hostPort += ":80"
+ }
+ }
+ return hostPort, hostNoPort
+}
+
+// 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 = *DefaultDialer
+
+// DialContext creates a new client connection. Use requestHeader to specify the
+// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
+// Use the response.Header to get the selected subprotocol
+// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
+//
+// The context will be used in the request and in the Dialer.
+//
+// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
+// non-nil *http.Response so that callers can handle redirects, authentication,
+// etcetera. The response body may not contain the entire response and does not
+// need to be closed by the application.
+func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
+ if d == nil {
+ d = &nilDialer
+ }
+
+ challengeKey, err := generateChallengeKey()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ u, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ 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: http.MethodGet,
+ URL: u,
+ Proto: "HTTP/1.1",
+ ProtoMajor: 1,
+ ProtoMinor: 1,
+ Header: make(http.Header),
+ Host: u.Host,
+ }
+ req = req.WithContext(ctx)
+
+ // 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"}
+ }
+
+ if d.HandshakeTimeout != 0 {
+ var cancel func()
+ ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
+ defer cancel()
+ }
+
+ // Get network dial function.
+ var netDial func(network, add string) (net.Conn, error)
+
+ switch u.Scheme {
+ case "http":
+ if d.NetDialContext != nil {
+ netDial = func(network, addr string) (net.Conn, error) {
+ return d.NetDialContext(ctx, network, addr)
+ }
+ } else if d.NetDial != nil {
+ netDial = d.NetDial
+ }
+ case "https":
+ if d.NetDialTLSContext != nil {
+ netDial = func(network, addr string) (net.Conn, error) {
+ return d.NetDialTLSContext(ctx, network, addr)
+ }
+ } else if d.NetDialContext != nil {
+ netDial = func(network, addr string) (net.Conn, error) {
+ return d.NetDialContext(ctx, network, addr)
+ }
+ } else if d.NetDial != nil {
+ netDial = d.NetDial
+ }
+ default:
+ return nil, nil, errMalformedURL
+ }
+
+ if netDial == nil {
+ netDialer := &net.Dialer{}
+ netDial = func(network, addr string) (net.Conn, error) {
+ return netDialer.DialContext(ctx, network, addr)
+ }
+ }
+
+ // If needed, wrap the dial function to set the connection deadline.
+ if deadline, ok := ctx.Deadline(); ok {
+ 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)
+ trace := httptrace.ContextClientTrace(ctx)
+ if trace != nil && trace.GetConn != nil {
+ trace.GetConn(hostPort)
+ }
+
+ netConn, err := netDial("tcp", hostPort)
+ if err != nil {
+ return nil, nil, err
+ }
+ if trace != nil && trace.GotConn != nil {
+ trace.GotConn(httptrace.GotConnInfo{
+ Conn: netConn,
+ })
+ }
+
+ defer func() {
+ if netConn != nil {
+ netConn.Close()
+ }
+ }()
+
+ if u.Scheme == "https" && d.NetDialTLSContext == nil {
+ // If NetDialTLSContext is set, assume that the TLS handshake has already been done
+
+ cfg := cloneTLSConfig(d.TLSClientConfig)
+ if cfg.ServerName == "" {
+ cfg.ServerName = hostNoPort
+ }
+ tlsConn := tls.Client(netConn, cfg)
+ netConn = tlsConn
+
+ if trace != nil && trace.TLSHandshakeStart != nil {
+ trace.TLSHandshakeStart()
+ }
+ err := doHandshake(ctx, tlsConn, cfg)
+ if trace != nil && trace.TLSHandshakeDone != nil {
+ trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
+ }
+
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
+
+ if err := req.Write(netConn); err != nil {
+ return nil, nil, err
+ }
+
+ if trace != nil && trace.GotFirstResponseByte != nil {
+ if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
+ trace.GotFirstResponseByte()
+ }
+ }
+
+ resp, err := http.ReadResponse(conn.br, req)
+ if err != nil {
+ if d.TLSClientConfig != nil {
+ for _, proto := range d.TLSClientConfig.NextProtos {
+ if proto != "http/1.1" {
+ return nil, nil, fmt.Errorf(
+ "websocket: protocol %q was given but is not supported;"+
+ "sharing tls.Config with net/http Transport can cause this error: %w",
+ proto, err,
+ )
+ }
+ }
+ }
+ return nil, nil, err
+ }
+
+ if d.Jar != nil {
+ if rc := resp.Cookies(); len(rc) > 0 {
+ d.Jar.SetCookies(u, rc)
+ }
+ }
+
+ if resp.StatusCode != 101 ||
+ !tokenListContainsValue(resp.Header, "Upgrade", "websocket") ||
+ !tokenListContainsValue(resp.Header, "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
+ }
+
+ for _, ext := range parseExtensions(resp.Header) {
+ if ext[""] != "permessage-deflate" {
+ continue
+ }
+ _, 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
+}
+
+func cloneTLSConfig(cfg *tls.Config) *tls.Config {
+ if cfg == nil {
+ return &tls.Config{}
+ }
+ return cfg.Clone()
+}
diff --git a/vendor/github.com/gorilla/websocket/compression.go b/vendor/github.com/gorilla/websocket/compression.go
new file mode 100644
index 0000000000..813ffb1e84
--- /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
new file mode 100644
index 0000000000..5161ef81f6
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/conn.go
@@ -0,0 +1,1238 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "bufio"
+ "encoding/binary"
+ "errors"
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "net"
+ "strconv"
+ "strings"
+ "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
+
+ writeWait = time.Second
+
+ defaultReadBufferSize = 4096
+ defaultWriteBufferSize = 4096
+
+ continuationFrame = 0
+ noFrame = -1
+)
+
+// Close codes defined in RFC 6455, section 11.7.
+const (
+ CloseNormalClosure = 1000
+ CloseGoingAway = 1001
+ CloseProtocolError = 1002
+ CloseUnsupportedData = 1003
+ CloseNoStatusReceived = 1005
+ CloseAbnormalClosure = 1006
+ CloseInvalidFramePayloadData = 1007
+ ClosePolicyViolation = 1008
+ CloseMessageTooBig = 1009
+ CloseMandatoryExtension = 1010
+ CloseInternalServerErr = 1011
+ CloseServiceRestart = 1012
+ CloseTryAgainLater = 1013
+ CloseTLSHandshake = 1015
+)
+
+// The message types are defined in RFC 6455, section 11.8.
+const (
+ // TextMessage denotes a text data message. The text message payload is
+ // interpreted as UTF-8 encoded text data.
+ TextMessage = 1
+
+ // BinaryMessage denotes a binary data message.
+ BinaryMessage = 2
+
+ // CloseMessage denotes a close control message. The optional message
+ // payload contains a numeric code and text. Use the FormatCloseMessage
+ // function to format a close message payload.
+ CloseMessage = 8
+
+ // PingMessage denotes a ping control message. The optional message payload
+ // is UTF-8 encoded text.
+ PingMessage = 9
+
+ // PongMessage denotes a pong control message. The optional message payload
+ // is UTF-8 encoded text.
+ PongMessage = 10
+)
+
+// ErrCloseSent is returned when the application writes a message to the
+// connection after sending a close message.
+var ErrCloseSent = errors.New("websocket: close sent")
+
+// ErrReadLimit is returned when reading a message that is larger than the
+// read limit set for the connection.
+var ErrReadLimit = errors.New("websocket: read limit exceeded")
+
+// netError satisfies the net Error interface.
+type netError struct {
+ msg string
+ temporary bool
+ timeout bool
+}
+
+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 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
+}
+
+// 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, 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()}
+ }
+ return err
+}
+
+func isControl(frameType int) bool {
+ return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage
+}
+
+func isData(frameType int) bool {
+ return frameType == TextMessage || frameType == BinaryMessage
+}
+
+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 isValidReceivedCloseCode(code int) bool {
+ return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
+}
+
+// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
+// interface. The type of the value stored in a pool is not specified.
+type BufferPool interface {
+ // Get gets a value from the pool or returns nil if the pool is empty.
+ Get() interface{}
+ // Put adds a value to the pool.
+ Put(interface{})
+}
+
+// writePoolData is the type added to the write buffer pool. This wrapper is
+// used to prevent applications from peeking at and depending on the values
+// added to the pool.
+type writePoolData struct{ buf []byte }
+
+// The Conn type represents a WebSocket connection.
+type Conn struct {
+ conn net.Conn
+ isServer bool
+ subprotocol string
+
+ // Write fields
+ mu chan struct{} // used as mutex to protect write to conn
+ writeBuf []byte // frame is constructed in this buffer.
+ writePool BufferPool
+ writeBufSize int
+ writeDeadline time.Time
+ writer io.WriteCloser // the current writer returned to the application
+ isWriting bool // for best-effort concurrent write detection
+
+ 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
+ // bytes remaining in current frame.
+ // set setReadRemaining to safely update this value and prevent overflow
+ readRemaining int64
+ readFinal bool // true the current message has more frames.
+ 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, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
+
+ if br == nil {
+ if readBufferSize == 0 {
+ readBufferSize = defaultReadBufferSize
+ } else if readBufferSize < maxControlFramePayloadSize {
+ // must be large enough for control frame
+ readBufferSize = maxControlFramePayloadSize
+ }
+ br = bufio.NewReaderSize(conn, readBufferSize)
+ }
+
+ if writeBufferSize <= 0 {
+ writeBufferSize = defaultWriteBufferSize
+ }
+ writeBufferSize += maxFrameHeaderSize
+
+ if writeBuf == nil && writeBufferPool == nil {
+ writeBuf = make([]byte, writeBufferSize)
+ }
+
+ mu := make(chan struct{}, 1)
+ mu <- struct{}{}
+ c := &Conn{
+ isServer: isServer,
+ br: br,
+ conn: conn,
+ mu: mu,
+ readFinal: true,
+ writeBuf: writeBuf,
+ writePool: writeBufferPool,
+ writeBufSize: writeBufferSize,
+ enableWriteCompression: true,
+ compressionLevel: defaultCompressionLevel,
+ }
+ c.SetCloseHandler(nil)
+ c.SetPingHandler(nil)
+ c.SetPongHandler(nil)
+ return c
+}
+
+// setReadRemaining tracks the number of bytes remaining on the connection. If n
+// overflows, an ErrReadLimit is returned.
+func (c *Conn) setReadRemaining(n int64) error {
+ if n < 0 {
+ return ErrReadLimit
+ }
+
+ c.readRemaining = n
+ return nil
+}
+
+// Subprotocol returns the negotiated protocol for the connection.
+func (c *Conn) Subprotocol() string {
+ return c.subprotocol
+}
+
+// Close closes the underlying network connection without sending or waiting
+// for a close message.
+func (c *Conn) Close() error {
+ return c.conn.Close()
+}
+
+// LocalAddr returns the local network address.
+func (c *Conn) LocalAddr() net.Addr {
+ return c.conn.LocalAddr()
+}
+
+// RemoteAddr returns the remote network address.
+func (c *Conn) RemoteAddr() net.Addr {
+ return c.conn.RemoteAddr()
+}
+
+// Write methods
+
+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) 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
+}
+
+func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error {
+ <-c.mu
+ defer func() { c.mu <- struct{}{} }()
+
+ c.writeErrMu.Lock()
+ err := c.writeErr
+ c.writeErrMu.Unlock()
+ if err != nil {
+ return err
+ }
+
+ c.conn.SetWriteDeadline(deadline)
+ 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
+}
+
+func (c *Conn) writeBufs(bufs ...[]byte) error {
+ b := net.Buffers(bufs)
+ _, err := b.WriteTo(c.conn)
+ return err
+}
+
+// WriteControl writes a control message with the given deadline. The allowed
+// message types are CloseMessage, PingMessage and PongMessage.
+func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error {
+ if !isControl(messageType) {
+ return errBadWriteOpCode
+ }
+ if len(data) > maxControlFramePayloadSize {
+ return errInvalidControlFrame
+ }
+
+ b0 := byte(messageType) | finalBit
+ b1 := byte(len(data))
+ if !c.isServer {
+ b1 |= maskBit
+ }
+
+ buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize)
+ buf = append(buf, b0, b1)
+
+ if c.isServer {
+ buf = append(buf, data...)
+ } else {
+ key := newMaskKey()
+ buf = append(buf, key[:]...)
+ buf = append(buf, data...)
+ maskBytes(key, 0, buf[6:])
+ }
+
+ d := 1000 * time.Hour
+ if !deadline.IsZero() {
+ d = deadline.Sub(time.Now())
+ if d < 0 {
+ return errWriteTimeout
+ }
+ }
+
+ timer := time.NewTimer(d)
+ select {
+ case <-c.mu:
+ timer.Stop()
+ case <-timer.C:
+ return errWriteTimeout
+ }
+ defer func() { c.mu <- struct{}{} }()
+
+ c.writeErrMu.Lock()
+ err := c.writeErr
+ c.writeErrMu.Unlock()
+ if err != nil {
+ return err
+ }
+
+ c.conn.SetWriteDeadline(deadline)
+ _, err = c.conn.Write(buf)
+ if err != nil {
+ return c.writeFatal(err)
+ }
+ if messageType == CloseMessage {
+ c.writeFatal(ErrCloseSent)
+ }
+ return err
+}
+
+// beginMessage prepares a connection and message writer for a new message.
+func (c *Conn) beginMessage(mw *messageWriter, 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()
+ if err != nil {
+ return err
+ }
+
+ mw.c = c
+ mw.frameType = messageType
+ mw.pos = maxFrameHeaderSize
+
+ if c.writeBuf == nil {
+ wpd, ok := c.writePool.Get().(writePoolData)
+ if ok {
+ c.writeBuf = wpd.buf
+ } else {
+ c.writeBuf = make([]byte, c.writeBufSize)
+ }
+ }
+ return nil
+}
+
+// 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.
+//
+// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and
+// PongMessage) are supported.
+func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
+ var mw messageWriter
+ if err := c.beginMessage(&mw, messageType); err != nil {
+ return nil, err
+ }
+ 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
+}
+
+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 (w *messageWriter) endMessage(err error) error {
+ if w.err != nil {
+ return err
+ }
+ c := w.c
+ w.err = err
+ c.writer = nil
+ if c.writePool != nil {
+ c.writePool.Put(writePoolData{buf: c.writeBuf})
+ c.writeBuf = 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(w.frameType) &&
+ (!final || length > maxControlFramePayloadSize) {
+ return w.endMessage(errInvalidControlFrame)
+ }
+
+ b0 := byte(w.frameType)
+ if final {
+ b0 |= finalBit
+ }
+ if w.compress {
+ b0 |= rsv1Bit
+ }
+ w.compress = false
+
+ b1 := byte(0)
+ if !c.isServer {
+ b1 |= maskBit
+ }
+
+ // Assume that the frame starts at beginning of c.writeBuf.
+ framePos := 0
+ if c.isServer {
+ // Adjust up if mask not included in the header.
+ framePos = 4
+ }
+
+ switch {
+ case length >= 65536:
+ c.writeBuf[framePos] = b0
+ c.writeBuf[framePos+1] = b1 | 127
+ binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length))
+ case length > 125:
+ framePos += 6
+ c.writeBuf[framePos] = b0
+ c.writeBuf[framePos+1] = b1 | 126
+ binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length))
+ default:
+ framePos += 8
+ c.writeBuf[framePos] = b0
+ c.writeBuf[framePos+1] = b1 | byte(length)
+ }
+
+ if !c.isServer {
+ key := newMaskKey()
+ copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
+ maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
+ if len(extra) > 0 {
+ return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode")))
+ }
+ }
+
+ // Write the buffers to the connection with best-effort detection of
+ // concurrent writes. See the concurrency section in the package
+ // documentation for more info.
+
+ if c.isWriting {
+ panic("concurrent write to websocket connection")
+ }
+ c.isWriting = true
+
+ err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra)
+
+ if !c.isWriting {
+ panic("concurrent write to websocket connection")
+ }
+ c.isWriting = false
+
+ if err != nil {
+ return w.endMessage(err)
+ }
+
+ if final {
+ w.endMessage(errWriteClosed)
+ 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.pos
+ if n <= 0 {
+ if err := w.flushFrame(false, nil); err != nil {
+ return 0, err
+ }
+ n = len(w.c.writeBuf) - w.pos
+ }
+ if n > max {
+ n = max
+ }
+ return n, nil
+}
+
+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.flushFrame(false, p)
+ if err != nil {
+ return 0, err
+ }
+ return len(p), nil
+ }
+
+ nn := len(p)
+ for len(p) > 0 {
+ n, err := w.ncopy(len(p))
+ if err != nil {
+ return 0, err
+ }
+ copy(w.c.writeBuf[w.pos:], p[:n])
+ w.pos += n
+ p = p[n:]
+ }
+ return nn, nil
+}
+
+func (w *messageWriter) WriteString(p string) (int, error) {
+ if w.err != nil {
+ return 0, w.err
+ }
+
+ nn := len(p)
+ for len(p) > 0 {
+ n, err := w.ncopy(len(p))
+ if err != nil {
+ return 0, err
+ }
+ 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 w.err != nil {
+ return 0, w.err
+ }
+ for {
+ 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.pos:])
+ w.pos += n
+ nn += int64(n)
+ if err != nil {
+ if err == io.EOF {
+ err = nil
+ }
+ break
+ }
+ }
+ return nn, err
+}
+
+func (w *messageWriter) Close() error {
+ if w.err != nil {
+ return w.err
+ }
+ return w.flushFrame(true, 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
+ }
+ 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 {
+
+ if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
+ // Fast path with no allocations and single frame.
+
+ var mw messageWriter
+ if err := c.beginMessage(&mw, messageType); err != nil {
+ return err
+ }
+ 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
+ }
+ if _, err = w.Write(data); err != nil {
+ return err
+ }
+ return w.Close()
+}
+
+// SetWriteDeadline sets the write deadline on the underlying network
+// connection. After a write has timed out, the websocket state is corrupt and
+// all future writes will return an error. A zero value for t means writes will
+// not time out.
+func (c *Conn) SetWriteDeadline(t time.Time) error {
+ c.writeDeadline = t
+ return nil
+}
+
+// Read methods
+
+func (c *Conn) advanceFrame() (int, error) {
+ // 1. Skip remainder of previous frame.
+
+ if c.readRemaining > 0 {
+ if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil {
+ return noFrame, err
+ }
+ }
+
+ // 2. Read and parse first two bytes of frame header.
+ // To aid debugging, collect and report all errors in the first two bytes
+ // of the header.
+
+ var errors []string
+
+ p, err := c.read(2)
+ if err != nil {
+ return noFrame, err
+ }
+
+ frameType := int(p[0] & 0xf)
+ final := p[0]&finalBit != 0
+ rsv1 := p[0]&rsv1Bit != 0
+ rsv2 := p[0]&rsv2Bit != 0
+ rsv3 := p[0]&rsv3Bit != 0
+ mask := p[1]&maskBit != 0
+ c.setReadRemaining(int64(p[1] & 0x7f))
+
+ c.readDecompress = false
+ if rsv1 {
+ if c.newDecompressionReader != nil {
+ c.readDecompress = true
+ } else {
+ errors = append(errors, "RSV1 set")
+ }
+ }
+
+ if rsv2 {
+ errors = append(errors, "RSV2 set")
+ }
+
+ if rsv3 {
+ errors = append(errors, "RSV3 set")
+ }
+
+ switch frameType {
+ case CloseMessage, PingMessage, PongMessage:
+ if c.readRemaining > maxControlFramePayloadSize {
+ errors = append(errors, "len > 125 for control")
+ }
+ if !final {
+ errors = append(errors, "FIN not set on control")
+ }
+ case TextMessage, BinaryMessage:
+ if !c.readFinal {
+ errors = append(errors, "data before FIN")
+ }
+ c.readFinal = final
+ case continuationFrame:
+ if c.readFinal {
+ errors = append(errors, "continuation after FIN")
+ }
+ c.readFinal = final
+ default:
+ errors = append(errors, "bad opcode "+strconv.Itoa(frameType))
+ }
+
+ if mask != c.isServer {
+ errors = append(errors, "bad MASK")
+ }
+
+ if len(errors) > 0 {
+ return noFrame, c.handleProtocolError(strings.Join(errors, ", "))
+ }
+
+ // 3. Read and parse frame length as per
+ // https://tools.ietf.org/html/rfc6455#section-5.2
+ //
+ // The length of the "Payload data", in bytes: if 0-125, that is the payload
+ // length.
+ // - If 126, the following 2 bytes interpreted as a 16-bit unsigned
+ // integer are the payload length.
+ // - If 127, the following 8 bytes interpreted as
+ // a 64-bit unsigned integer (the most significant bit MUST be 0) are the
+ // payload length. Multibyte length quantities are expressed in network byte
+ // order.
+
+ switch c.readRemaining {
+ case 126:
+ p, err := c.read(2)
+ if err != nil {
+ return noFrame, err
+ }
+
+ if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil {
+ return noFrame, err
+ }
+ case 127:
+ p, err := c.read(8)
+ if err != nil {
+ return noFrame, err
+ }
+
+ if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil {
+ return noFrame, err
+ }
+ }
+
+ // 4. Handle frame masking.
+
+ if mask {
+ c.readMaskPos = 0
+ 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.
+
+ if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
+
+ c.readLength += c.readRemaining
+ // Don't allow readLength to overflow in the presence of a large readRemaining
+ // counter.
+ if c.readLength < 0 {
+ return noFrame, ErrReadLimit
+ }
+
+ if c.readLimit > 0 && c.readLength > c.readLimit {
+ c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
+ return noFrame, ErrReadLimit
+ }
+
+ return frameType, nil
+ }
+
+ // 6. Read control frame payload.
+
+ var payload []byte
+ if c.readRemaining > 0 {
+ payload, err = c.read(int(c.readRemaining))
+ c.setReadRemaining(0)
+ if err != nil {
+ return noFrame, err
+ }
+ if c.isServer {
+ maskBytes(c.readMaskKey, 0, payload)
+ }
+ }
+
+ // 7. Process control frame payload.
+
+ switch frameType {
+ case PongMessage:
+ if err := c.handlePong(string(payload)); err != nil {
+ return noFrame, err
+ }
+ case PingMessage:
+ if err := c.handlePing(string(payload)); err != nil {
+ return noFrame, err
+ }
+ case CloseMessage:
+ closeCode := CloseNoStatusReceived
+ closeText := ""
+ if len(payload) >= 2 {
+ closeCode = int(binary.BigEndian.Uint16(payload))
+ if !isValidReceivedCloseCode(closeCode) {
+ return noFrame, c.handleProtocolError("bad close code " + strconv.Itoa(closeCode))
+ }
+ closeText = string(payload[2:])
+ if !utf8.ValidString(closeText) {
+ return noFrame, c.handleProtocolError("invalid utf8 payload in close frame")
+ }
+ }
+ if err := c.handleClose(closeCode, closeText); err != nil {
+ return noFrame, err
+ }
+ return noFrame, &CloseError{Code: closeCode, Text: closeText}
+ }
+
+ return frameType, nil
+}
+
+func (c *Conn) handleProtocolError(message string) error {
+ data := FormatCloseMessage(CloseProtocolError, message)
+ if len(data) > maxControlFramePayloadSize {
+ data = data[:maxControlFramePayloadSize]
+ }
+ c.WriteControl(CloseMessage, data, time.Now().Add(writeWait))
+ return errors.New("websocket: " + message)
+}
+
+// NextReader returns the next data message received from the peer. The
+// returned messageType is either TextMessage or BinaryMessage.
+//
+// There can be at most one open reader on a connection. NextReader discards
+// the previous message if the application has not already consumed it.
+//
+// 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.messageReader = nil
+ c.readLength = 0
+
+ for c.readErr == nil {
+ frameType, err := c.advanceFrame()
+ if err != nil {
+ c.readErr = hideTempErr(err)
+ break
+ }
+
+ if frameType == TextMessage || frameType == BinaryMessage {
+ c.messageReader = &messageReader{c}
+ c.reader = c.messageReader
+ if c.readDecompress {
+ c.reader = c.newDecompressionReader(c.reader)
+ }
+ return frameType, c.reader, nil
+ }
+ }
+
+ // 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
+}
+
+type messageReader struct{ c *Conn }
+
+func (r *messageReader) Read(b []byte) (int, error) {
+ c := r.c
+ if c.messageReader != r {
+ return 0, io.EOF
+ }
+
+ for c.readErr == nil {
+
+ 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])
+ }
+ rem := c.readRemaining
+ rem -= int64(n)
+ c.setReadRemaining(rem)
+ if c.readRemaining > 0 && c.readErr == io.EOF {
+ c.readErr = errUnexpectedEOF
+ }
+ return n, c.readErr
+ }
+
+ if c.readFinal {
+ c.messageReader = nil
+ return 0, io.EOF
+ }
+
+ frameType, err := c.advanceFrame()
+ switch {
+ case err != nil:
+ c.readErr = hideTempErr(err)
+ case frameType == TextMessage || frameType == BinaryMessage:
+ c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
+ }
+ }
+
+ 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) {
+ var r io.Reader
+ messageType, r, err = c.NextReader()
+ if err != nil {
+ return messageType, nil, err
+ }
+ p, err = ioutil.ReadAll(r)
+ return messageType, p, err
+}
+
+// SetReadDeadline sets the read deadline on the underlying network connection.
+// After a read has timed out, the websocket connection state is corrupt and
+// all future reads will return an error. A zero value for t means reads will
+// not time out.
+func (c *Conn) SetReadDeadline(t time.Time) error {
+ return c.conn.SetReadDeadline(t)
+}
+
+// SetReadLimit sets the maximum size in bytes for a message read from the peer. If a
+// 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 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 {
+ 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 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 }
+ }
+ c.handlePong = h
+}
+
+// NetConn returns the underlying connection that is wrapped by c.
+// Note that writing to or reading from this connection directly will corrupt the
+// WebSocket connection.
+func (c *Conn) NetConn() net.Conn {
+ return c.conn
+}
+
+// UnderlyingConn returns the internal net.Conn. This can be used to further
+// modifications to connection specific flags.
+// Deprecated: Use the NetConn method.
+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)
+ return buf
+}
diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go
new file mode 100644
index 0000000000..8db0cef95a
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/doc.go
@@ -0,0 +1,227 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package websocket implements the WebSocket protocol defined in RFC 6455.
+//
+// Overview
+//
+// 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,
+// WriteBufferSize: 1024,
+// }
+//
+// func handler(w http.ResponseWriter, r *http.Request) {
+// conn, err := upgrader.Upgrade(w, r, nil)
+// if err != nil {
+// log.Println(err)
+// return
+// }
+// ... Use conn to send and receive messages.
+// }
+//
+// Call the connection's WriteMessage and ReadMessage methods to send and
+// receive messages as a slice of bytes. This snippet of code shows how to echo
+// messages using these methods:
+//
+// for {
+// messageType, p, err := conn.ReadMessage()
+// if err != nil {
+// log.Println(err)
+// return
+// }
+// if err := conn.WriteMessage(messageType, p); err != nil {
+// log.Println(err)
+// return
+// }
+// }
+//
+// In above snippet of code, p is a []byte and messageType is an int with value
+// websocket.BinaryMessage or websocket.TextMessage.
+//
+// An application can also send and receive messages using the io.WriteCloser
+// and io.Reader interfaces. To send a message, call the connection NextWriter
+// 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
+// shows how to echo messages using the NextWriter and NextReader methods:
+//
+// for {
+// messageType, r, err := conn.NextReader()
+// if err != nil {
+// return
+// }
+// w, err := conn.NextWriter(messageType)
+// if err != nil {
+// return err
+// }
+// if _, err := io.Copy(w, r); err != nil {
+// return err
+// }
+// if err := w.Close(); err != nil {
+// return err
+// }
+// }
+//
+// Data Messages
+//
+// The WebSocket protocol distinguishes between text and binary data messages.
+// Text messages are interpreted as UTF-8 encoded text. The interpretation of
+// binary messages is left to the application.
+//
+// This package uses the TextMessage and BinaryMessage integer constants to
+// identify the two data message types. The ReadMessage and NextReader methods
+// return the type of the received message. The messageType argument to the
+// WriteMessage and NextWriter methods specifies the type of a sent message.
+//
+// It is the application's responsibility to ensure that text messages are
+// valid UTF-8 encoded text.
+//
+// Control Messages
+//
+// The WebSocket protocol defines three types of control messages: close, ping
+// and pong. Call the connection WriteControl, WriteMessage or NextWriter
+// methods to send a control message to the peer.
+//
+// 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 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.
+//
+// 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.
+//
+// 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 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 {
+// if _, _, err := c.NextReader(); err != nil {
+// c.Close()
+// break
+// }
+// }
+// }
+//
+// 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
+// any host. It's up to the server to enforce an origin policy using the Origin
+// request header sent by the browser.
+//
+// The Upgrader calls the function specified in the CheckOrigin field to check
+// the origin. If the CheckOrigin function returns false, then the Upgrade
+// 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 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.
+//
+// Buffers
+//
+// Connections buffer network input and output to reduce the number
+// of system calls when reading or writing messages.
+//
+// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
+// Section 5 for a discussion of message framing. A WebSocket frame header is
+// written to the network each time a write buffer is flushed to the network.
+// Decreasing the size of the write buffer can increase the amount of framing
+// overhead on the connection.
+//
+// The buffer sizes in bytes are specified by the ReadBufferSize and
+// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
+// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
+// buffers created by the HTTP server when a buffer size field is set to zero.
+// The HTTP server buffers have a size of 4096 at the time of this writing.
+//
+// The buffer sizes do not limit the size of a message that can be read or
+// written by a connection.
+//
+// Buffers are held for the lifetime of the connection by default. If the
+// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
+// write buffer only when writing a message.
+//
+// Applications should tune the buffer sizes to balance memory use and
+// performance. Increasing the buffer size uses more memory, but can reduce the
+// number of system calls to read or write the network. In the case of writing,
+// increasing the buffer size can reduce the number of frame headers written to
+// the network.
+//
+// Some guidelines for setting buffer parameters are:
+//
+// Limit the buffer sizes to the maximum expected message size. Buffers larger
+// than the largest message do not provide any benefit.
+//
+// Depending on the distribution of message sizes, setting the buffer size to
+// a value less than the maximum expected message size can greatly reduce memory
+// use with a small impact on performance. Here's an example: If 99% of the
+// messages are smaller than 256 bytes and the maximum message size is 512
+// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
+// than a buffer size of 512 bytes. The memory savings is 50%.
+//
+// A write buffer pool is useful when the application has a modest number
+// writes over a large number of connections. when buffers are pooled, a larger
+// buffer size has a reduced impact on total memory use and has the benefit of
+// reducing system calls and frame overhead.
+//
+// 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:
+//
+// conn.EnableWriteCompression(false)
+//
+// 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.
+//
+// Use of compression is experimental and may result in decreased performance.
+package websocket
diff --git a/vendor/github.com/gorilla/websocket/join.go b/vendor/github.com/gorilla/websocket/join.go
new file mode 100644
index 0000000000..c64f8c8290
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/join.go
@@ -0,0 +1,42 @@
+// Copyright 2019 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 (
+ "io"
+ "strings"
+)
+
+// JoinMessages concatenates received messages to create a single io.Reader.
+// The string term is appended to each message. The returned reader does not
+// support concurrent calls to the Read method.
+func JoinMessages(c *Conn, term string) io.Reader {
+ return &joinReader{c: c, term: term}
+}
+
+type joinReader struct {
+ c *Conn
+ term string
+ r io.Reader
+}
+
+func (r *joinReader) Read(p []byte) (int, error) {
+ if r.r == nil {
+ var err error
+ _, r.r, err = r.c.NextReader()
+ if err != nil {
+ return 0, err
+ }
+ if r.term != "" {
+ r.r = io.MultiReader(r.r, strings.NewReader(r.term))
+ }
+ }
+ n, err := r.r.Read(p)
+ if err == io.EOF {
+ err = nil
+ r.r = nil
+ }
+ return n, err
+}
diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go
new file mode 100644
index 0000000000..dc2c1f6415
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/json.go
@@ -0,0 +1,60 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "encoding/json"
+ "io"
+)
+
+// 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 as a message.
+//
+// See the documentation for encoding/json Marshal for details about the
+// conversion of Go values to JSON.
+func (c *Conn) WriteJSON(v interface{}) error {
+ w, err := c.NextWriter(TextMessage)
+ if err != nil {
+ return err
+ }
+ err1 := json.NewEncoder(w).Encode(v)
+ err2 := w.Close()
+ if err1 != nil {
+ return err1
+ }
+ return err2
+}
+
+// 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)
+}
+
+// ReadJSON reads the next JSON-encoded message from the connection and stores
+// it in the value pointed to by v.
+//
+// See the documentation for the encoding/json Unmarshal function for details
+// about the conversion of JSON to a Go value.
+func (c *Conn) ReadJSON(v interface{}) error {
+ _, r, err := c.NextReader()
+ if err != nil {
+ return err
+ }
+ err = json.NewDecoder(r).Decode(v)
+ if err == io.EOF {
+ // 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 0000000000..d0742bf2a5
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/mask.go
@@ -0,0 +1,55 @@
+// 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.
+
+//go:build !appengine
+// +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 0000000000..36250ca7c4
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/mask_safe.go
@@ -0,0 +1,16 @@
+// 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.
+
+//go:build appengine
+// +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 0000000000..c854225e96
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/prepared.go
@@ -0,0 +1,102 @@
+// 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
+ 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 struct{}, 1)
+ mu <- struct{}{}
+ 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 0000000000..e0f466b72f
--- /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, forwardDial: forwardDialer.Dial}, nil
+ })
+}
+
+type httpProxyDialer struct {
+ proxyURL *url.URL
+ forwardDial 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.forwardDial(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: http.MethodConnect,
+ 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
new file mode 100644
index 0000000000..bb33597432
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/server.go
@@ -0,0 +1,365 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "bufio"
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// HandshakeError describes an error with the handshake from the peer.
+type HandshakeError struct {
+ message string
+}
+
+func (e HandshakeError) Error() string { return e.message }
+
+// Upgrader specifies parameters for upgrading an HTTP connection to a
+// WebSocket connection.
+//
+// It is safe to call Upgrader's methods concurrently.
+type Upgrader struct {
+ // HandshakeTimeout specifies the duration for the handshake to complete.
+ HandshakeTimeout time.Duration
+
+ // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
+ // 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
+
+ // WriteBufferPool is a pool of buffers for write operations. If the value
+ // is not set, then write buffers are allocated to the connection for the
+ // lifetime of the connection.
+ //
+ // A pool is most useful when the application has a modest volume of writes
+ // across a large number of connections.
+ //
+ // Applications should use a single pool for each unique value of
+ // WriteBufferSize.
+ WriteBufferPool BufferPool
+
+ // Subprotocols specifies the server's supported protocols in order of
+ // preference. If this field is not nil, then the Upgrade method negotiates a
+ // subprotocol by selecting the first match in this list with a protocol
+ // requested by the client. If there's no match, then no protocol is
+ // negotiated (the Sec-Websocket-Protocol header is not included in the
+ // handshake response).
+ Subprotocols []string
+
+ // Error specifies the function for generating HTTP error responses. If Error
+ // is nil, then http.Error is used to generate the HTTP response.
+ 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, 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) {
+ err := HandshakeError{reason}
+ 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
+}
+
+// checkSameOrigin returns true if the origin is not set or is equal to the request host.
+func checkSameOrigin(r *http.Request) bool {
+ origin := r.Header["Origin"]
+ if len(origin) == 0 {
+ return true
+ }
+ u, err := url.Parse(origin[0])
+ if err != nil {
+ return false
+ }
+ return equalASCIIFold(u.Host, r.Host)
+}
+
+func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
+ if u.Subprotocols != nil {
+ clientProtocols := Subprotocols(r)
+ for _, serverProtocol := range u.Subprotocols {
+ for _, clientProtocol := range clientProtocols {
+ if clientProtocol == serverProtocol {
+ return clientProtocol
+ }
+ }
+ }
+ } else if responseHeader != nil {
+ return responseHeader.Get("Sec-Websocket-Protocol")
+ }
+ return ""
+}
+
+// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
+//
+// The responseHeader is included in the response to the client's upgrade
+// request. Use the responseHeader to specify cookies (Set-Cookie). To specify
+// subprotocols supported by the server, set Upgrader.Subprotocols directly.
+//
+// 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) {
+ const badHandshake = "websocket: the client is not using the websocket protocol: "
+
+ if !tokenListContainsValue(r.Header, "Connection", "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, badHandshake+"'websocket' token not found in 'Upgrade' header")
+ }
+
+ if r.Method != http.MethodGet {
+ 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
+ if checkOrigin == nil {
+ checkOrigin = checkSameOrigin
+ }
+ if !checkOrigin(r) {
+ return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
+ }
+
+ challengeKey := r.Header.Get("Sec-Websocket-Key")
+ if !isValidChallengeKey(challengeKey) {
+ return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header must be Base64 encoded value of 16-byte in length")
+ }
+
+ 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
+ }
+ }
+
+ h, ok := w.(http.Hijacker)
+ if !ok {
+ return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
+ }
+ var brw *bufio.ReadWriter
+ netConn, brw, err := h.Hijack()
+ if err != nil {
+ return u.returnError(w, r, http.StatusInternalServerError, err.Error())
+ }
+
+ if brw.Reader.Buffered() > 0 {
+ netConn.Close()
+ return nil, errors.New("websocket: client sent data before handshake is complete")
+ }
+
+ var br *bufio.Reader
+ if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
+ // Reuse hijacked buffered reader as connection reader.
+ br = brw.Reader
+ }
+
+ buf := bufioWriterBuffer(netConn, brw.Writer)
+
+ var writeBuf []byte
+ if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
+ // Reuse hijacked write buffer as connection buffer.
+ writeBuf = buf
+ }
+
+ c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
+ c.subprotocol = subprotocol
+
+ if compress {
+ c.newCompressionWriter = compressNoContextTakeover
+ c.newDecompressionReader = decompressNoContextTakeover
+ }
+
+ // Use larger of hijacked buffer and connection write buffer for header.
+ p := buf
+ if len(c.writeBuf) > len(p) {
+ p = c.writeBuf
+ }
+ p = p[: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, 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
+ }
+ for _, v := range vs {
+ p = append(p, k...)
+ p = append(p, ": "...)
+ for i := 0; i < len(v); i++ {
+ b := v[i]
+ if b <= 31 {
+ // prevent response splitting.
+ b = ' '
+ }
+ p = append(p, b)
+ }
+ p = append(p, "\r\n"...)
+ }
+ }
+ p = append(p, "\r\n"...)
+
+ // Clear deadlines set by HTTP server.
+ netConn.SetDeadline(time.Time{})
+
+ if u.HandshakeTimeout > 0 {
+ netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
+ }
+ if _, err = netConn.Write(p); err != nil {
+ netConn.Close()
+ return nil, err
+ }
+ if u.HandshakeTimeout > 0 {
+ netConn.SetWriteDeadline(time.Time{})
+ }
+
+ return c, nil
+}
+
+// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
+//
+// Deprecated: Use websocket.Upgrader instead.
+//
+// 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", http.StatusForbidden)
+// return
+// }
+//
+// If the endpoint supports subprotocols, then the application is responsible
+// for negotiating the protocol used on the connection. Use the Subprotocols()
+// function to get the subprotocols requested by the client. Use the
+// Sec-Websocket-Protocol response header to specify the subprotocol selected
+// by the application.
+//
+// The responseHeader is included in the response to the client's upgrade
+// request. Use the responseHeader to specify cookies (Set-Cookie) and the
+// negotiated subprotocol (Sec-Websocket-Protocol).
+//
+// The connection buffers IO to the underlying network connection. The
+// readBufSize and writeBufSize parameters specify the size of the buffers to
+// use. Messages can be larger than the buffers.
+//
+// If the request is not a valid WebSocket handshake, then Upgrade returns an
+// error of type HandshakeError. Applications should handle this error by
+// replying to the client with an HTTP error response.
+func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) {
+ u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
+ u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
+ // don't return errors to maintain backwards compatibility
+ }
+ u.CheckOrigin = func(r *http.Request) bool {
+ // allow all connections by default
+ return true
+ }
+ return u.Upgrade(w, r, responseHeader)
+}
+
+// Subprotocols returns the subprotocols requested by the client in the
+// Sec-Websocket-Protocol header.
+func Subprotocols(r *http.Request) []string {
+ h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol"))
+ if h == "" {
+ return nil
+ }
+ protocols := strings.Split(h, ",")
+ for i := range protocols {
+ protocols[i] = strings.TrimSpace(protocols[i])
+ }
+ 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")
+}
+
+// bufioReaderSize size returns the size of a bufio.Reader.
+func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
+ // This code assumes that peek on a reset reader returns
+ // bufio.Reader.buf[:0].
+ // TODO: Use bufio.Reader.Size() after Go 1.10
+ br.Reset(originalReader)
+ if p, err := br.Peek(0); err == nil {
+ return cap(p)
+ }
+ return 0
+}
+
+// writeHook is an io.Writer that records the last slice passed to it vio
+// io.Writer.Write.
+type writeHook struct {
+ p []byte
+}
+
+func (wh *writeHook) Write(p []byte) (int, error) {
+ wh.p = p
+ return len(p), nil
+}
+
+// bufioWriterBuffer grabs the buffer from a bufio.Writer.
+func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
+ // This code assumes that bufio.Writer.buf[:1] is passed to the
+ // bufio.Writer's underlying writer.
+ var wh writeHook
+ bw.Reset(&wh)
+ bw.WriteByte(0)
+ bw.Flush()
+
+ bw.Reset(originalWriter)
+
+ return wh.p[:cap(wh.p)]
+}
diff --git a/vendor/github.com/gorilla/websocket/tls_handshake.go b/vendor/github.com/gorilla/websocket/tls_handshake.go
new file mode 100644
index 0000000000..a62b68ccb1
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/tls_handshake.go
@@ -0,0 +1,21 @@
+//go:build go1.17
+// +build go1.17
+
+package websocket
+
+import (
+ "context"
+ "crypto/tls"
+)
+
+func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error {
+ if err := tlsConn.HandshakeContext(ctx); err != nil {
+ return err
+ }
+ if !cfg.InsecureSkipVerify {
+ if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/vendor/github.com/gorilla/websocket/tls_handshake_116.go b/vendor/github.com/gorilla/websocket/tls_handshake_116.go
new file mode 100644
index 0000000000..e1b2b44f6e
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/tls_handshake_116.go
@@ -0,0 +1,21 @@
+//go:build !go1.17
+// +build !go1.17
+
+package websocket
+
+import (
+ "context"
+ "crypto/tls"
+)
+
+func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error {
+ if err := tlsConn.Handshake(); err != nil {
+ return err
+ }
+ if !cfg.InsecureSkipVerify {
+ if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go
new file mode 100644
index 0000000000..31a5dee646
--- /dev/null
+++ b/vendor/github.com/gorilla/websocket/util.go
@@ -0,0 +1,298 @@
+// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package websocket
+
+import (
+ "crypto/rand"
+ "crypto/sha1"
+ "encoding/base64"
+ "io"
+ "net/http"
+ "strings"
+ "unicode/utf8"
+)
+
+var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
+
+func computeAcceptKey(challengeKey string) string {
+ h := sha1.New()
+ h.Write([]byte(challengeKey))
+ h.Write(keyGUID)
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
+
+func generateChallengeKey() (string, error) {
+ p := make([]byte, 16)
+ if _, err := io.ReadFull(rand.Reader, p); err != nil {
+ return "", err
+ }
+ return base64.StdEncoding.EncodeToString(p), nil
+}
+
+// Token octets per RFC 2616.
+var isTokenOctet = [256]bool{
+ '!': true,
+ '#': true,
+ '$': true,
+ '%': true,
+ '&': true,
+ '\'': true,
+ '*': true,
+ '+': true,
+ '-': true,
+ '.': true,
+ '0': true,
+ '1': true,
+ '2': true,
+ '3': true,
+ '4': true,
+ '5': true,
+ '6': true,
+ '7': true,
+ '8': true,
+ '9': true,
+ 'A': true,
+ 'B': true,
+ 'C': true,
+ 'D': true,
+ 'E': true,
+ 'F': true,
+ 'G': true,
+ 'H': true,
+ 'I': true,
+ 'J': true,
+ 'K': true,
+ 'L': true,
+ 'M': true,
+ 'N': true,
+ 'O': true,
+ 'P': true,
+ 'Q': true,
+ 'R': true,
+ 'S': true,
+ 'T': true,
+ 'U': true,
+ 'W': true,
+ 'V': true,
+ 'X': true,
+ 'Y': true,
+ 'Z': true,
+ '^': true,
+ '_': true,
+ '`': true,
+ 'a': true,
+ 'b': true,
+ 'c': true,
+ 'd': true,
+ 'e': true,
+ 'f': true,
+ 'g': true,
+ 'h': true,
+ 'i': true,
+ 'j': true,
+ 'k': true,
+ 'l': true,
+ 'm': true,
+ 'n': true,
+ 'o': true,
+ 'p': true,
+ 'q': true,
+ 'r': true,
+ 's': true,
+ 't': true,
+ 'u': true,
+ 'v': true,
+ 'w': true,
+ 'x': true,
+ 'y': true,
+ 'z': true,
+ '|': true,
+ '~': true,
+}
+
+// skipSpace returns a slice of the string s with all leading RFC 2616 linear
+// whitespace removed.
+func skipSpace(s string) (rest string) {
+ i := 0
+ for ; i < len(s); i++ {
+ if b := s[i]; b != ' ' && b != '\t' {
+ break
+ }
+ }
+ return s[i:]
+}
+
+// nextToken returns the leading RFC 2616 token of s and the string following
+// the token.
+func nextToken(s string) (token, rest string) {
+ i := 0
+ for ; i < len(s); i++ {
+ if !isTokenOctet[s[i]] {
+ break
+ }
+ }
+ return s[:i], s[i:]
+}
+
+// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616
+// and the string following the token or quoted string.
+func nextTokenOrQuoted(s string) (value string, rest string) {
+ if !strings.HasPrefix(s, "\"") {
+ return nextToken(s)
+ }
+ s = s[1:]
+ for i := 0; i < len(s); i++ {
+ switch s[i] {
+ case '"':
+ return s[:i], s[i+1:]
+ case '\\':
+ p := make([]byte, len(s)-1)
+ j := copy(p, s[:i])
+ escape := true
+ for i = i + 1; i < len(s); i++ {
+ b := s[i]
+ switch {
+ case escape:
+ escape = false
+ p[j] = b
+ j++
+ case b == '\\':
+ escape = true
+ case b == '"':
+ return string(p[:j]), s[i+1:]
+ default:
+ p[j] = b
+ j++
+ }
+ }
+ return "", ""
+ }
+ }
+ return "", ""
+}
+
+// equalASCIIFold returns true if s is equal to t with ASCII case folding as
+// defined in RFC 4790.
+func equalASCIIFold(s, t string) bool {
+ for s != "" && t != "" {
+ sr, size := utf8.DecodeRuneInString(s)
+ s = s[size:]
+ tr, size := utf8.DecodeRuneInString(t)
+ t = t[size:]
+ if sr == tr {
+ continue
+ }
+ if 'A' <= sr && sr <= 'Z' {
+ sr = sr + 'a' - 'A'
+ }
+ if 'A' <= tr && tr <= 'Z' {
+ tr = tr + 'a' - 'A'
+ }
+ if sr != tr {
+ return false
+ }
+ }
+ return s == t
+}
+
+// tokenListContainsValue returns true if the 1#token header with the given
+// name contains a token equal to value with ASCII case folding.
+func tokenListContainsValue(header http.Header, name string, value string) bool {
+headers:
+ for _, s := range header[name] {
+ for {
+ var t string
+ t, s = nextToken(skipSpace(s))
+ if t == "" {
+ continue headers
+ }
+ s = skipSpace(s)
+ if s != "" && s[0] != ',' {
+ continue headers
+ }
+ if equalASCIIFold(t, value) {
+ return true
+ }
+ if s == "" {
+ continue headers
+ }
+ s = s[1:]
+ }
+ }
+ return false
+}
+
+// parseExtensions parses WebSocket extensions from a header.
+func parseExtensions(header http.Header) []map[string]string {
+ // From RFC 6455:
+ //
+ // Sec-WebSocket-Extensions = extension-list
+ // extension-list = 1#extension
+ // extension = extension-token *( ";" extension-param )
+ // extension-token = registered-token
+ // registered-token = token
+ // extension-param = token [ "=" (token | quoted-string) ]
+ // ;When using the quoted-string syntax variant, the value
+ // ;after quoted-string unescaping MUST conform to the
+ // ;'token' ABNF.
+
+ var result []map[string]string
+headers:
+ for _, s := range header["Sec-Websocket-Extensions"] {
+ for {
+ var t string
+ t, s = nextToken(skipSpace(s))
+ if t == "" {
+ continue headers
+ }
+ ext := map[string]string{"": t}
+ for {
+ s = skipSpace(s)
+ if !strings.HasPrefix(s, ";") {
+ break
+ }
+ var k string
+ k, s = nextToken(skipSpace(s[1:]))
+ if k == "" {
+ continue headers
+ }
+ s = skipSpace(s)
+ var v string
+ if strings.HasPrefix(s, "=") {
+ v, s = nextTokenOrQuoted(skipSpace(s[1:]))
+ s = skipSpace(s)
+ }
+ if s != "" && s[0] != ',' && s[0] != ';' {
+ continue headers
+ }
+ ext[k] = v
+ }
+ if s != "" && s[0] != ',' {
+ continue headers
+ }
+ result = append(result, ext)
+ if s == "" {
+ continue headers
+ }
+ s = s[1:]
+ }
+ }
+ return result
+}
+
+// isValidChallengeKey checks if the argument meets RFC6455 specification.
+func isValidChallengeKey(s string) bool {
+ // From RFC6455:
+ //
+ // A |Sec-WebSocket-Key| header field with a base64-encoded (see
+ // Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in
+ // length.
+
+ if s == "" {
+ return false
+ }
+ decoded, err := base64.StdEncoding.DecodeString(s)
+ return err == nil && len(decoded) == 16
+}
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 0000000000..2e668f6b88
--- /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/modules.txt b/vendor/modules.txt
index 62feb87cf3..58fc706b38 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -843,6 +843,9 @@ github.com/gorilla/mux
# github.com/gorilla/schema v1.4.1
## explicit; go 1.20
github.com/gorilla/schema
+# github.com/gorilla/websocket v1.5.3
+## explicit; go 1.12
+github.com/gorilla/websocket
# github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
## explicit; go 1.14
github.com/grpc-ecosystem/go-grpc-middleware