81 lines
2.2 KiB
Go
81 lines
2.2 KiB
Go
package api
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/gorilla/websocket"
|
||
|
|
)
|
||
|
|
|
||
|
|
const wsBearerProtocol = "bearer"
|
||
|
|
|
||
|
|
// extractWSToken resolves the access token without putting it in the query string.
|
||
|
|
// Preference order: Authorization Bearer → Sec-WebSocket-Protocol → httpOnly cookie.
|
||
|
|
func extractWSToken(r *http.Request) (token string, selectedProtocol string) {
|
||
|
|
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||
|
|
return strings.TrimSpace(strings.TrimPrefix(auth, "Bearer ")), ""
|
||
|
|
}
|
||
|
|
|
||
|
|
protocols := websocket.Subprotocols(r)
|
||
|
|
for i, p := range protocols {
|
||
|
|
p = strings.TrimSpace(p)
|
||
|
|
if p == wsBearerProtocol && i+1 < len(protocols) {
|
||
|
|
next := strings.TrimSpace(protocols[i+1])
|
||
|
|
if next != "" {
|
||
|
|
return next, wsBearerProtocol
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if strings.HasPrefix(p, wsBearerProtocol+".") {
|
||
|
|
tok := strings.TrimPrefix(p, wsBearerProtocol+".")
|
||
|
|
if tok != "" {
|
||
|
|
// Negotiate the bare "bearer" name so the JWT is not echoed in the response.
|
||
|
|
return tok, wsBearerProtocol
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if c, err := r.Cookie(cookieAccess); err == nil && c.Value != "" {
|
||
|
|
return c.Value, ""
|
||
|
|
}
|
||
|
|
|
||
|
|
// Query-string tokens are rejected — JWTs in URLs end up in proxy/access logs.
|
||
|
|
return "", ""
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleWS upgrades to a websocket authenticated via Bearer header or
|
||
|
|
// Sec-WebSocket-Protocol (bearer + token).
|
||
|
|
func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !s.allowWSHandshake(r) {
|
||
|
|
w.Header().Set("Retry-After", "60")
|
||
|
|
writeErr(w, http.StatusTooManyRequests, authRateMsg)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
token, selectedProtocol := extractWSToken(r)
|
||
|
|
if token == "" {
|
||
|
|
writeErr(w, http.StatusUnauthorized, "توکن ارسال نشده است")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
user, err := s.auth.parseAndLoad(r.Context(), token)
|
||
|
|
if err != nil {
|
||
|
|
writeErr(w, http.StatusUnauthorized, "توکن نامعتبر است")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if meta, ok := r.Context().Value(ctxMeta).(*requestMeta); ok && meta != nil {
|
||
|
|
meta.UserID = user.ID
|
||
|
|
}
|
||
|
|
|
||
|
|
upgrader := s.newUpgrader()
|
||
|
|
if selectedProtocol != "" {
|
||
|
|
upgrader.Subprotocols = []string{selectedProtocol}
|
||
|
|
}
|
||
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||
|
|
if err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
c := s.hub.Register(user.ID, conn)
|
||
|
|
|
||
|
|
// send a welcome frame so clients know the socket is live
|
||
|
|
c.send <- []byte(`{"type":"connected"}`)
|
||
|
|
}
|