Add websocket handler

This commit is contained in:
Jannis Mattheis
2020-10-04 19:26:06 +02:00
parent 7298f124c8
commit 81743cbb4d
20 changed files with 1049 additions and 0 deletions
+29
View File
@@ -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())
}
+24
View File
@@ -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
View File
@@ -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")
}
+5
View File
@@ -0,0 +1,5 @@
package ws
type Event interface {
Execute(*Rooms, ClientInfo) error
}
+36
View File
@@ -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
}
+36
View File
@@ -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
}
+73
View File
@@ -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
}
+42
View File
@@ -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
}
+36
View File
@@ -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
}
+36
View File
@@ -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
}
+54
View File
@@ -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
}
+31
View File
@@ -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
}
+37
View File
@@ -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
}
+30
View File
@@ -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
View File
@@ -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
}
+43
View File
@@ -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
}
}
+92
View File
@@ -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"
)
+52
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}
}