mirror of
https://github.com/screego/server.git
synced 2026-08-29 04:20:43 +08:00
Add websocket handler
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func RandString(length int) string {
|
||||
res := make([]byte, length)
|
||||
for i := range res {
|
||||
index := randIntn(len(tokenCharacters))
|
||||
res[i] = tokenCharacters[index]
|
||||
}
|
||||
return string(res)
|
||||
}
|
||||
|
||||
var (
|
||||
tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_!@#$%^&*()){}\\/=+,.><")
|
||||
randReader = rand.Reader
|
||||
)
|
||||
|
||||
func randIntn(n int) int {
|
||||
max := big.NewInt(int64(n))
|
||||
res, err := rand.Int(randReader, max)
|
||||
if err != nil {
|
||||
panic("random source is not available")
|
||||
}
|
||||
return int(res.Int64())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Inspired by https://github.com/TheDeveloper/sillyname/blob/master/index.js#L18
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var adjectives = []string{"Black", "White", "Gray", "Brown", "Red", "Pink", "Crimson", "Carnelian", "Orange", "Yellow", "Ivory", "Cream", "Green", "Viridian", "Aquamarine", "Cyan", "Blue", "Cerulean", "Azure", "Indigo", "Navy", "Violet", "Purple", "Lavender", "Magenta", "Rainbow", "Iridescent", "Spectrum", "Prism", "Bold", "Vivid", "Pale", "Clear", "Glass", "Translucent", "Misty", "Dark", "Light", "Gold", "Silver", "Copper", "Bronze", "Steel", "Iron", "Brass", "Mercury", "Zinc", "Chrome", "Platinum", "Titanium", "Nickel", "Lead", "Pewter", "Rust", "Metal", "Stone", "Quartz", "Granite", "Marble", "Alabaster", "Agate", "Jasper", "Pebble", "Pyrite", "Crystal", "Geode", "Obsidian", "Mica", "Flint", "Sand", "Gravel", "Boulder", "Basalt", "Ruby", "Beryl", "Scarlet", "Citrine", "Sulpher", "Topaz", "Amber", "Emerald", "Malachite", "Jade", "Abalone", "Lapis", "Sapphire", "Diamond", "Peridot", "Gem", "Jewel", "Bevel", "Coral", "Jet", "Ebony", "Wood", "Tree", "Cherry", "Maple", "Cedar", "Branch", "Bramble", "Rowan", "Ash", "Fir", "Pine", "Cactus", "Alder", "Grove", "Forest", "Jungle", "Palm", "Bush", "Mulberry", "Juniper", "Vine", "Ivy", "Rose", "Lily", "Tulip", "Daffodil", "Honeysuckle", "Fuschia", "Hazel", "Walnut", "Almond", "Lime", "Lemon", "Apple", "Blossom", "Bloom", "Crocus", "Rose", "Buttercup", "Dandelion", "Iris", "Carnation", "Fern", "Root", "Branch", "Leaf", "Seed", "Flower", "Petal", "Pollen", "Orchid", "Mangrove", "Cypress", "Sequoia", "Sage", "Heather", "Snapdragon", "Daisy", "Mountain", "Hill", "Alpine", "Chestnut", "Valley", "Glacier", "Forest", "Grove", "Glen", "Tree", "Thorn", "Stump", "Desert", "Canyon", "Dune", "Oasis", "Mirage", "Well", "Spring", "Meadow", "Field", "Prairie", "Grass", "Tundra", "Island", "Shore", "Sand", "Shell", "Surf", "Wave", "Foam", "Tide", "Lake", "River", "Brook", "Stream", "Pool", "Pond", "Sun", "Sprinkle", "Shade", "Shadow", "Rain", "Cloud", "Storm", "Hail", "Snow", "Sleet", "Thunder", "Lightning", "Wind", "Hurricane", "Typhoon", "Dawn", "Sunrise", "Morning", "Noon", "Twilight", "Evening", "Sunset", "Midnight", "Night", "Sky", "Star", "Stellar", "Comet", "Nebula", "Quasar", "Solar", "Lunar", "Planet", "Meteor", "Sprout", "Pear", "Plum", "Kiwi", "Berry", "Apricot", "Peach", "Mango", "Pineapple", "Coconut", "Olive", "Ginger", "Root", "Plain", "Fancy", "Stripe", "Spot", "Speckle", "Spangle", "Ring", "Band", "Blaze", "Paint", "Pinto", "Shade", "Tabby", "Brindle", "Patch", "Calico", "Checker", "Dot", "Pattern", "Glitter", "Glimmer", "Shimmer", "Dull", "Dust", "Dirt", "Glaze", "Scratch", "Quick", "Swift", "Fast", "Slow", "Clever", "Fire", "Flicker", "Flash", "Spark", "Ember", "Coal", "Flame", "Chocolate", "Vanilla", "Sugar", "Spice", "Cake", "Pie", "Cookie", "Candy", "Caramel", "Spiral", "Round", "Jelly", "Square", "Narrow", "Long", "Short", "Small", "Tiny", "Big", "Giant", "Great", "Atom", "Peppermint", "Mint", "Butter", "Fringe", "Rag", "Quilt", "Truth", "Lie", "Holy", "Curse", "Noble", "Sly", "Brave", "Shy", "Lava", "Foul", "Leather", "Fantasy", "Keen", "Luminous", "Feather", "Sticky", "Gossamer", "Cotton", "Rattle", "Silk", "Satin", "Cord", "Denim", "Flannel", "Plaid", "Wool", "Linen", "Silent", "Flax", "Weak", "Valiant", "Fierce", "Gentle", "Rhinestone", "Splash", "North", "South", "East", "West", "Summer", "Winter", "Autumn", "Spring", "Season", "Equinox", "Solstice", "Paper", "Motley", "Torch", "Ballistic", "Rampant", "Shag", "Freckle", "Wild", "Free", "Chain", "Sheer", "Crazy", "Mad", "Candle", "Ribbon", "Lace", "Notch", "Wax", "Shine", "Shallow", "Deep", "Bubble", "Harvest", "Fluff", "Venom", "Boom", "Slash", "Rune", "Cold", "Quill", "Love", "Hate", "Garnet", "Zircon", "Power", "Bone", "Void", "Horn", "Glory", "Cyber", "Nova", "Hot", "Helix", "Cosmic", "Quark", "Quiver", "Holly", "Clover", "Polar", "Regal", "Ripple", "Ebony", "Wheat", "Phantom", "Dew", "Chisel", "Crack", "Chatter", "Laser", "Foil", "Tin", "Clever", "Treasure", "Maze", "Twisty", "Curly", "Fortune", "Fate", "Destiny", "Cute", "Slime", "Ink", "Disco", "Plume", "Time", "Psychadelic", "Relic", "Fossil", "Water", "Savage", "Ancient", "Rapid", "Road", "Trail", "Stitch", "Button", "Bow", "Nimble", "Zest", "Sour", "Bitter", "Phase", "Fan", "Frill", "Plump", "Pickle", "Mud", "Puddle", "Pond", "River", "Spring", "Stream", "Battle", "Arrow", "Plume", "Roan", "Pitch", "Tar", "Cat", "Dog", "Horse", "Lizard", "Bird", "Fish", "Saber", "Scythe", "Sharp", "Soft", "Razor", "Neon", "Dandy", "Weed", "Swamp", "Marsh", "Bog", "Peat", "Moor", "Muck", "Mire", "Grave", "Fair", "Just", "Brick", "Puzzle", "Skitter", "Prong", "Fork", "Dent", "Dour", "Warp", "Luck", "Coffee", "Split", "Chip", "Hollow", "Heavy", "Legend", "Hickory", "Mesquite", "Nettle", "Rogue", "Charm", "Prickle", "Bead", "Sponge", "Whip", "Bald", "Frost", "Fog", "Oil", "Veil", "Cliff", "Volcano", "Rift", "Maze", "Proud", "Dew", "Mirror", "Shard", "Salt", "Pepper", "Honey", "Thread", "Bristle", "Ripple", "Glow", "Zenith"}
|
||||
|
||||
var nouns = []string{"head", "crest", "crown", "tooth", "fang", "horn", "frill", "skull", "bone", "tongue", "throat", "voice", "nose", "snout", "chin", "eye", "sight", "seer", "speaker", "singer", "song", "chanter", "howler", "chatter", "shrieker", "shriek", "jaw", "bite", "biter", "neck", "shoulder", "fin", "wing", "arm", "lifter", "grasp", "grabber", "hand", "paw", "foot", "finger", "toe", "thumb", "talon", "palm", "touch", "racer", "runner", "hoof", "fly", "flier", "swoop", "roar", "hiss", "hisser", "snarl", "dive", "diver", "rib", "chest", "back", "ridge", "leg", "legs", "tail", "beak", "walker", "lasher", "swisher", "carver", "kicker", "roarer", "crusher", "spike", "shaker", "charger", "hunter", "weaver", "crafter", "binder", "scribe", "muse", "snap", "snapper", "slayer", "stalker", "track", "tracker", "scar", "scarer", "fright", "killer", "death", "doom", "healer", "saver", "friend", "foe", "guardian", "thunder", "lightning", "cloud", "storm", "forger", "scale", "hair", "braid", "nape", "belly", "thief", "stealer", "reaper", "giver", "taker", "dancer", "player", "gambler", "twister", "turner", "painter", "dart", "drifter", "sting", "stinger", "venom", "spur", "ripper", "swallow", "devourer", "knight", "lady", "lord", "queen", "king", "master", "mistress", "prince", "princess", "duke", "dutchess", "samurai", "ninja", "knave", "slave", "servant", "sage", "wizard", "witch", "warlock", "warrior", "jester", "paladin", "bard", "trader", "sword", "shield", "knife", "dagger", "arrow", "bow", "fighter", "bane", "follower", "leader", "scourge", "watcher", "cat", "panther", "tiger", "cougar", "puma", "jaguar", "ocelot", "lynx", "lion", "leopard", "ferret", "weasel", "wolverine", "bear", "raccoon", "dog", "wolf", "kitten", "puppy", "cub", "fox", "hound", "terrier", "coyote", "hyena", "jackal", "pig", "horse", "donkey", "stallion", "mare", "zebra", "antelope", "gazelle", "deer", "buffalo", "bison", "boar", "elk", "whale", "dolphin", "shark", "fish", "minnow", "salmon", "ray", "fisher", "otter", "gull", "duck", "goose", "crow", "raven", "bird", "eagle", "raptor", "hawk", "falcon", "moose", "heron", "owl", "stork", "crane", "sparrow", "robin", "parrot", "cockatoo", "carp", "lizard", "gecko", "iguana", "snake", "python", "viper", "boa", "condor", "vulture", "spider", "fly", "scorpion", "heron", "oriole", "toucan", "bee", "wasp", "hornet", "rabbit", "bunny", "hare", "brow", "mustang", "ox", "piper", "soarer", "flasher", "moth", "mask", "hide", "hero", "antler", "chill", "chiller", "gem", "ogre", "myth", "elf", "fairy", "pixie", "dragon", "griffin", "unicorn", "pegasus", "sprite", "fancier", "chopper", "slicer", "skinner", "butterfly", "legend", "wanderer", "rover", "raver", "loon", "lancer", "glass", "glazer", "flame", "crystal", "lantern", "lighter", "cloak", "bell", "ringer", "keeper", "centaur", "bolt", "catcher", "whimsey", "quester", "rat", "mouse", "serpent", "wyrm", "gargoyle", "thorn", "whip", "rider", "spirit", "sentry", "bat", "beetle", "burn", "cowl", "stone", "gem", "collar", "mark", "grin", "scowl", "spear", "razor", "edge", "seeker", "jay", "ape", "monkey", "gorilla", "koala", "kangaroo", "yak", "sloth", "ant", "roach", "weed", "seed", "eater", "razor", "shirt", "face", "goat", "mind", "shift", "rider", "face", "mole", "vole", "pirate", "llama", "stag", "bug", "cap", "boot", "drop", "hugger", "sargent", "snagglefoot", "carpet", "curtain"}
|
||||
|
||||
func noun() string {
|
||||
return nouns[rand.Intn(len(nouns)-1)]
|
||||
}
|
||||
|
||||
func adjective() string {
|
||||
return adjectives[rand.Intn(len(adjectives)-1)]
|
||||
}
|
||||
|
||||
func NewName() string {
|
||||
return strings.Title(noun() + adjective() + " " + adjective())
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/xid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
var ping = func(conn *websocket.Conn) error {
|
||||
return conn.WriteMessage(websocket.PingMessage, nil)
|
||||
}
|
||||
|
||||
var writeJSON = func(conn *websocket.Conn, v interface{}) error {
|
||||
return conn.WriteJSON(v)
|
||||
}
|
||||
|
||||
const (
|
||||
writeWait = 2 * time.Second
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *websocket.Conn
|
||||
info ClientInfo
|
||||
once once
|
||||
read chan<- ClientMessage
|
||||
}
|
||||
|
||||
type ClientMessage struct {
|
||||
Info ClientInfo
|
||||
Incoming Event
|
||||
}
|
||||
|
||||
type ClientInfo struct {
|
||||
ID xid.ID
|
||||
RoomID string
|
||||
Authenticated bool
|
||||
Write chan outgoing.Message
|
||||
Close chan string
|
||||
Addr net.IP
|
||||
}
|
||||
|
||||
func newClient(conn *websocket.Conn, req *http.Request, read chan ClientMessage, authenticated, trustProxy bool) *Client {
|
||||
conn.SetCloseHandler(func(code int, text string) error {
|
||||
message := websocket.FormatCloseMessage(code, text)
|
||||
log.Debug().Str("reason", text).Int("code", code).Msg("WebSocket Close")
|
||||
return conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(writeWait))
|
||||
})
|
||||
|
||||
ip := conn.RemoteAddr().(*net.TCPAddr).IP
|
||||
if realIP := req.Header.Get("X-Real-IP"); trustProxy && realIP != "" {
|
||||
ip = net.ParseIP(realIP)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
info: ClientInfo{
|
||||
Authenticated: authenticated,
|
||||
ID: xid.New(),
|
||||
RoomID: "",
|
||||
Addr: ip,
|
||||
Write: make(chan outgoing.Message, 1),
|
||||
Close: make(chan string, 1),
|
||||
},
|
||||
read: read,
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection.
|
||||
func (c *Client) Close() {
|
||||
c.once.Do(func() {
|
||||
c.conn.Close()
|
||||
close(c.info.Write)
|
||||
c.read <- ClientMessage{
|
||||
Info: c.info,
|
||||
Incoming: &Disconnected{},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// startWriteHandler starts listening on the client connection. As we do not need anything from the client,
|
||||
// we ignore incoming messages. Leaves the loop on errors.
|
||||
func (c *Client) startReading(pongWait time.Duration) {
|
||||
defer c.Close()
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.conn.SetPongHandler(func(appData string) error {
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
for {
|
||||
t, m, err := c.conn.NextReader()
|
||||
if err != nil {
|
||||
printWebSocketError("read", err)
|
||||
return
|
||||
}
|
||||
if t == websocket.BinaryMessage {
|
||||
_ = c.conn.CloseHandler()(websocket.CloseUnsupportedData, fmt.Sprintf("unsupported binary message type: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
incoming, err := ReadTypedIncoming(m)
|
||||
if err != nil {
|
||||
_ = c.conn.CloseHandler()(websocket.CloseNormalClosure, fmt.Sprintf("malformed message: %s", err))
|
||||
return
|
||||
}
|
||||
c.read <- ClientMessage{Info: c.info, Incoming: incoming}
|
||||
}
|
||||
}
|
||||
|
||||
// startWriteHandler starts the write loop. The method has the following tasks:
|
||||
// * ping the client in the interval provided as parameter
|
||||
// * write messages send by the channel to the client
|
||||
// * on errors exit the loop
|
||||
func (c *Client) startWriteHandler(pingPeriod time.Duration) {
|
||||
pingTicker := time.NewTicker(pingPeriod)
|
||||
defer c.Close()
|
||||
defer pingTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case reason := <-c.info.Close:
|
||||
_ = c.conn.CloseHandler()(websocket.CloseNormalClosure, reason)
|
||||
return
|
||||
case message, ok := <-c.info.Write:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
typed, err := ToTypedOutgoing(message)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("could not get typed message, exiting connection.")
|
||||
return
|
||||
}
|
||||
|
||||
if room, ok := message.(outgoing.Room); ok {
|
||||
c.info.RoomID = room.ID
|
||||
}
|
||||
|
||||
if err := writeJSON(c.conn, typed); err != nil {
|
||||
printWebSocketError("write", err)
|
||||
return
|
||||
}
|
||||
case <-pingTicker.C:
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := ping(c.conn); err != nil {
|
||||
printWebSocketError("ping", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printWebSocketError(typex string, err error) {
|
||||
|
||||
closeError, ok := err.(*websocket.CloseError)
|
||||
|
||||
if ok && closeError != nil && (closeError.Code == 1000 || closeError.Code == 1001) {
|
||||
// normal closure
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("type", typex).Err(err).Msg("WebSocket")
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ws
|
||||
|
||||
type Event interface {
|
||||
Execute(*Rooms, ClientInfo) error
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("clientanswer", func() Event {
|
||||
return &ClientAnswer{}
|
||||
})
|
||||
}
|
||||
|
||||
type ClientAnswer outgoing.P2PMessage
|
||||
|
||||
func (e *ClientAnswer) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
session, ok := room.Sessions[e.SID]
|
||||
|
||||
if !ok || session.Client != current.ID {
|
||||
return fmt.Errorf("session with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[session.Host].Write <- outgoing.ClientAnswer(*e)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("clientice", func() Event {
|
||||
return &ClientICE{}
|
||||
})
|
||||
}
|
||||
|
||||
type ClientICE outgoing.P2PMessage
|
||||
|
||||
func (e *ClientICE) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
session, ok := room.Sessions[e.SID]
|
||||
|
||||
if !ok || session.Client != current.ID {
|
||||
return fmt.Errorf("session with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[session.Host].Write <- outgoing.ClientICE(*e)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/screego/server/config"
|
||||
"github.com/screego/server/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("create", func() Event {
|
||||
return &Create{}
|
||||
})
|
||||
}
|
||||
|
||||
type Create struct {
|
||||
ID string `json:"id"`
|
||||
Mode ConnectionMode `json:"mode"`
|
||||
CloseOnOwnerLeave bool `json:"closeOnOwnerLeave"`
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
func (e *Create) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID != "" {
|
||||
return fmt.Errorf("cannot join room, you are already in one")
|
||||
}
|
||||
|
||||
if _, ok := rooms.Rooms[e.ID]; ok {
|
||||
return fmt.Errorf("room with id %s does already existn", e.ID)
|
||||
}
|
||||
|
||||
name := e.UserName
|
||||
if name == "" {
|
||||
name = util.NewName()
|
||||
}
|
||||
|
||||
switch rooms.config.AuthMode {
|
||||
case config.AuthModeNone:
|
||||
case config.AuthModeAll:
|
||||
if !current.Authenticated {
|
||||
return errors.New("you need to login")
|
||||
}
|
||||
case config.AuthModeTurn:
|
||||
if e.Mode != ConnectionSTUN && e.Mode != ConnectionLocal && !current.Authenticated {
|
||||
return errors.New("you need to login")
|
||||
}
|
||||
default:
|
||||
return errors.New("invalid authmode:" + rooms.config.AuthMode)
|
||||
}
|
||||
|
||||
room := &Room{
|
||||
ID: e.ID,
|
||||
CloseOnOwnerLeave: e.CloseOnOwnerLeave,
|
||||
Mode: e.Mode,
|
||||
Sessions: map[xid.ID]*RoomSession{},
|
||||
Users: map[xid.ID]*User{
|
||||
current.ID: {
|
||||
ID: current.ID,
|
||||
Name: name,
|
||||
Sharing: false,
|
||||
Owner: true,
|
||||
Addr: current.Addr,
|
||||
Write: current.Write,
|
||||
Close: current.Close,
|
||||
},
|
||||
},
|
||||
}
|
||||
rooms.Rooms[e.ID] = room
|
||||
room.notifyInfoChanged()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package ws
|
||||
|
||||
type Disconnected struct {
|
||||
}
|
||||
|
||||
func (e *Disconnected) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
// room may already be removed
|
||||
return nil
|
||||
}
|
||||
|
||||
user, ok := room.Users[current.ID]
|
||||
|
||||
if !ok {
|
||||
// room may already be removed
|
||||
return nil
|
||||
}
|
||||
|
||||
delete(room.Users, current.ID)
|
||||
|
||||
if user.Owner && room.CloseOnOwnerLeave {
|
||||
for _, member := range room.Users {
|
||||
member.Close <- "Owner left"
|
||||
}
|
||||
delete(rooms.Rooms, current.RoomID)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(room.Users) == 0 {
|
||||
delete(rooms.Rooms, current.RoomID)
|
||||
return nil
|
||||
}
|
||||
|
||||
room.notifyInfoChanged()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("hostice", func() Event {
|
||||
return &HostICE{}
|
||||
})
|
||||
}
|
||||
|
||||
type HostICE outgoing.P2PMessage
|
||||
|
||||
func (e *HostICE) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
session, ok := room.Sessions[e.SID]
|
||||
|
||||
if !ok || session.Host != current.ID {
|
||||
return fmt.Errorf("session with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[session.Client].Write <- outgoing.HostICE(*e)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("hostoffer", func() Event {
|
||||
return &HostOffer{}
|
||||
})
|
||||
}
|
||||
|
||||
type HostOffer outgoing.P2PMessage
|
||||
|
||||
func (e *HostOffer) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
session, ok := room.Sessions[e.SID]
|
||||
|
||||
if !ok || session.Host != current.ID {
|
||||
return fmt.Errorf("session with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[session.Client].Write <- outgoing.HostOffer(*e)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/screego/server/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("join", func() Event {
|
||||
return &Join{}
|
||||
})
|
||||
}
|
||||
|
||||
type Join struct {
|
||||
ID string `json:"id"`
|
||||
UserName string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Join) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID != "" {
|
||||
return fmt.Errorf("cannot join room, you are already in one")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[e.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", e.ID)
|
||||
}
|
||||
name := e.UserName
|
||||
if name == "" {
|
||||
name = util.NewName()
|
||||
}
|
||||
|
||||
room.Users[current.ID] = &User{
|
||||
ID: current.ID,
|
||||
Name: name,
|
||||
Sharing: false,
|
||||
Owner: false,
|
||||
Addr: current.Addr,
|
||||
Write: current.Write,
|
||||
Close: current.Close,
|
||||
}
|
||||
room.notifyInfoChanged()
|
||||
|
||||
for _, user := range room.Users {
|
||||
if current.ID == user.ID || !user.Sharing {
|
||||
continue
|
||||
}
|
||||
room.newSession(user.ID, current.ID, rooms)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("name", func() Event {
|
||||
return &Name{}
|
||||
})
|
||||
}
|
||||
|
||||
type Name struct {
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
func (e *Name) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[current.ID].Name = e.UserName
|
||||
|
||||
room.notifyInfoChanged()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("share", func() Event {
|
||||
return &StartShare{}
|
||||
})
|
||||
}
|
||||
|
||||
type StartShare struct {
|
||||
}
|
||||
|
||||
func (e *StartShare) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[current.ID].Sharing = true
|
||||
|
||||
for _, user := range room.Users {
|
||||
if current.ID == user.ID {
|
||||
continue
|
||||
}
|
||||
room.newSession(current.ID, user.ID, rooms)
|
||||
}
|
||||
|
||||
room.notifyInfoChanged()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
register("stopshare", func() Event {
|
||||
return &StopShare{}
|
||||
})
|
||||
}
|
||||
|
||||
type StopShare struct {
|
||||
}
|
||||
|
||||
func (e *StopShare) Execute(rooms *Rooms, current ClientInfo) error {
|
||||
if current.RoomID == "" {
|
||||
return fmt.Errorf("not in a room")
|
||||
}
|
||||
|
||||
room, ok := rooms.Rooms[current.RoomID]
|
||||
if !ok {
|
||||
return fmt.Errorf("room with id %s does not exist", current.RoomID)
|
||||
}
|
||||
|
||||
room.Users[current.ID].Sharing = false
|
||||
|
||||
room.notifyInfoChanged()
|
||||
return nil
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright 2009 The Go 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 ws
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Modified version of sync.Once (https://github.com/golang/go/blob/master/src/sync/once.go)
|
||||
// This version unlocks the mutex early and therefore doesn't hold the lock while executing func f().
|
||||
type once struct {
|
||||
m sync.Mutex
|
||||
done uint32
|
||||
}
|
||||
|
||||
func (o *once) Do(f func()) {
|
||||
if atomic.LoadUint32(&o.done) == 1 {
|
||||
return
|
||||
}
|
||||
if o.mayExecute() {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *once) mayExecute() bool {
|
||||
o.m.Lock()
|
||||
defer o.m.Unlock()
|
||||
if o.done == 0 {
|
||||
atomic.StoreUint32(&o.done, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Execute(t *testing.T) {
|
||||
executeOnce := once{}
|
||||
execution := make(chan struct{})
|
||||
fExecute := func() {
|
||||
execution <- struct{}{}
|
||||
}
|
||||
go executeOnce.Do(fExecute)
|
||||
go executeOnce.Do(fExecute)
|
||||
|
||||
select {
|
||||
case <-execution:
|
||||
// expected
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("fExecute should be executed once")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-execution:
|
||||
t.Fatal("should only execute once")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// expected
|
||||
}
|
||||
|
||||
assert.False(t, executeOnce.mayExecute())
|
||||
|
||||
go executeOnce.Do(fExecute)
|
||||
|
||||
select {
|
||||
case <-execution:
|
||||
t.Fatal("should only execute once")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// expected
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package outgoing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
Type() string
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
ID string `json:"id"`
|
||||
Mode ConnectionMode `json:"mode"`
|
||||
Users []User `json:"users"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID xid.ID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Sharing bool `json:"sharing"`
|
||||
You bool `json:"you"`
|
||||
Owner bool `json:"owner"`
|
||||
}
|
||||
|
||||
func (Room) Type() string {
|
||||
return "room"
|
||||
}
|
||||
|
||||
type HostSession struct {
|
||||
ID xid.ID `json:"id"`
|
||||
Peer xid.ID `json:"peer"`
|
||||
ICEServers []ICEServer `json:"iceServers"`
|
||||
}
|
||||
|
||||
func (HostSession) Type() string {
|
||||
return "hostsession"
|
||||
}
|
||||
|
||||
type ClientSession struct {
|
||||
ID xid.ID `json:"id"`
|
||||
Peer xid.ID `json:"peer"`
|
||||
ICEServers []ICEServer `json:"iceServers"`
|
||||
}
|
||||
|
||||
func (ClientSession) Type() string {
|
||||
return "clientsession"
|
||||
}
|
||||
|
||||
type ICEServer struct {
|
||||
URLs []string `json:"urls"`
|
||||
Credential string `json:"credential"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type P2PMessage struct {
|
||||
SID xid.ID `json:"sid"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
type HostICE P2PMessage
|
||||
|
||||
func (HostICE) Type() string {
|
||||
return "hostice"
|
||||
}
|
||||
|
||||
type ClientICE P2PMessage
|
||||
|
||||
func (ClientICE) Type() string {
|
||||
return "clientice"
|
||||
}
|
||||
|
||||
type ClientAnswer P2PMessage
|
||||
|
||||
func (ClientAnswer) Type() string {
|
||||
return "clientanswer"
|
||||
}
|
||||
|
||||
type HostOffer P2PMessage
|
||||
|
||||
func (HostOffer) Type() string {
|
||||
return "hostoffer"
|
||||
}
|
||||
|
||||
type ConnectionMode string
|
||||
|
||||
const (
|
||||
ConnectionLocal ConnectionMode = "local"
|
||||
ConnectionSTUN ConnectionMode = "stun"
|
||||
ConnectionTURN ConnectionMode = "turn"
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
type Typed struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
func ToTypedOutgoing(outgoing outgoing.Message) (Typed, error) {
|
||||
payload, err := json.Marshal(outgoing)
|
||||
if err != nil {
|
||||
return Typed{}, err
|
||||
}
|
||||
return Typed{
|
||||
Type: outgoing.Type(),
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReadTypedIncoming(r io.Reader) (Event, error) {
|
||||
typed := Typed{}
|
||||
if err := json.NewDecoder(r).Decode(&typed); err != nil {
|
||||
return nil, fmt.Errorf("%s e", err)
|
||||
}
|
||||
|
||||
create, ok := provider[typed.Type]
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("cannot handle " + typed.Type)
|
||||
}
|
||||
|
||||
payload := create()
|
||||
|
||||
if err := json.Unmarshal(typed.Payload, payload); err != nil {
|
||||
return nil, fmt.Errorf("kekw %s", err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
var provider = map[string]func() Event{}
|
||||
|
||||
func register(t string, incoming func() Event) {
|
||||
provider[t] = incoming
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/screego/server/config"
|
||||
"github.com/screego/server/util"
|
||||
"github.com/screego/server/ws/outgoing"
|
||||
)
|
||||
|
||||
type ConnectionMode string
|
||||
|
||||
const (
|
||||
ConnectionLocal ConnectionMode = "local"
|
||||
ConnectionSTUN ConnectionMode = "stun"
|
||||
ConnectionTURN ConnectionMode = config.AuthModeTurn
|
||||
)
|
||||
|
||||
type Room struct {
|
||||
ID string
|
||||
CloseOnOwnerLeave bool
|
||||
Mode ConnectionMode
|
||||
Users map[xid.ID]*User
|
||||
Sessions map[xid.ID]*RoomSession
|
||||
}
|
||||
|
||||
func (r *Room) newSession(host, client xid.ID, rooms *Rooms) {
|
||||
id := xid.New()
|
||||
r.Sessions[id] = &RoomSession{
|
||||
Host: host,
|
||||
Client: client,
|
||||
}
|
||||
|
||||
iceHost := []outgoing.ICEServer{}
|
||||
iceClient := []outgoing.ICEServer{}
|
||||
switch r.Mode {
|
||||
case ConnectionLocal:
|
||||
case ConnectionSTUN:
|
||||
iceHost = []outgoing.ICEServer{{URLs: []string{rooms.turnServer.StunAddress}}}
|
||||
iceClient = []outgoing.ICEServer{{URLs: []string{rooms.turnServer.StunAddress}}}
|
||||
case ConnectionTURN:
|
||||
hostPW := util.RandString(20)
|
||||
clientPW := util.RandString(20)
|
||||
hostName := id.String() + "host"
|
||||
rooms.turnServer.Allow(hostName, hostPW, r.Users[host].Addr)
|
||||
clientName := id.String() + "client"
|
||||
rooms.turnServer.Allow(clientName, clientPW, r.Users[client].Addr)
|
||||
iceHost = []outgoing.ICEServer{{
|
||||
URLs: []string{rooms.turnServer.TurnAddress},
|
||||
Credential: hostPW,
|
||||
Username: hostName,
|
||||
}}
|
||||
iceClient = []outgoing.ICEServer{{
|
||||
URLs: []string{rooms.turnServer.TurnAddress},
|
||||
Credential: clientPW,
|
||||
Username: clientName,
|
||||
}}
|
||||
|
||||
}
|
||||
r.Users[host].Write <- outgoing.HostSession{Peer: client, ID: id, ICEServers: iceHost}
|
||||
r.Users[client].Write <- outgoing.ClientSession{Peer: host, ID: id, ICEServers: iceClient}
|
||||
}
|
||||
|
||||
type RoomSession struct {
|
||||
Host xid.ID
|
||||
Client xid.ID
|
||||
}
|
||||
|
||||
func (r *Room) notifyInfoChanged() {
|
||||
for _, current := range r.Users {
|
||||
users := []outgoing.User{}
|
||||
for _, user := range r.Users {
|
||||
users = append(users, outgoing.User{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Sharing: user.Sharing,
|
||||
You: current == user,
|
||||
Owner: user.Owner,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
left := users[i]
|
||||
right := users[j]
|
||||
|
||||
if left.Owner != right.Owner {
|
||||
return left.Owner
|
||||
}
|
||||
|
||||
if left.Sharing != right.Sharing {
|
||||
return left.Sharing
|
||||
}
|
||||
|
||||
return left.Name < right.Name
|
||||
})
|
||||
|
||||
current.Write <- outgoing.Room{
|
||||
ID: r.ID,
|
||||
Users: users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID xid.ID
|
||||
Addr net.IP
|
||||
Name string
|
||||
Sharing bool
|
||||
Owner bool
|
||||
Write chan<- outgoing.Message
|
||||
Close chan<- string
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/screego/server/auth"
|
||||
"github.com/screego/server/config"
|
||||
"github.com/screego/server/turn"
|
||||
)
|
||||
|
||||
func NewRooms(tServer *turn.Server, users *auth.Users, conf config.Config) *Rooms {
|
||||
return &Rooms{
|
||||
Rooms: map[string]*Room{},
|
||||
Incoming: make(chan ClientMessage),
|
||||
turnServer: tServer,
|
||||
users: users,
|
||||
config: conf,
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
origin := r.Header.Get("origin")
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if u.Host == r.Host {
|
||||
return true
|
||||
}
|
||||
return conf.CheckOrigin(origin)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Rooms struct {
|
||||
turnServer *turn.Server
|
||||
Rooms map[string]*Room
|
||||
Incoming chan ClientMessage
|
||||
upgrader websocket.Upgrader
|
||||
users *auth.Users
|
||||
config config.Config
|
||||
}
|
||||
|
||||
func (r *Rooms) Upgrade(w http.ResponseWriter, req *http.Request) {
|
||||
conn, err := r.upgrader.Upgrade(w, req, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("Websocket upgrade")
|
||||
w.WriteHeader(400)
|
||||
_, _ = w.Write([]byte(fmt.Sprintf("Upgrade failed %s", err)))
|
||||
return
|
||||
}
|
||||
|
||||
_, loggedIn := r.users.CurrentUser(req)
|
||||
c := newClient(conn, req, r.Incoming, loggedIn, r.config.TrustProxyHeaders)
|
||||
|
||||
go c.startReading(time.Second * 20)
|
||||
go c.startWriteHandler(time.Second * 5)
|
||||
}
|
||||
|
||||
func (r *Rooms) Start() {
|
||||
for {
|
||||
msg := <-r.Incoming
|
||||
if err := msg.Incoming.Execute(r, msg.Info); err != nil {
|
||||
msg.Info.Close <- err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user