screen sharing for developers
## Intro In the past I've had some problems sharing my screen with coworkers using corporate chatting solutions like Microsoft Teams. I wanted to show them some of my code, but either the stream lagged several seconds behind or the quality was so poor that my colleagues couldn't read the code. Or both. That's why I created screego. It allows you to share your screen with good quality and low latency. Screego is an addition to existing software and only helps to share your screen. Nothing else (:. ## Features * Multi User Screenshare * Secure transfer via WebRTC * Low latency / High resolution * Simple Install via Docker / single binary * Integrated TURN Server see [NAT Traversal](https://screego.net/#/nat-traversal) [Demo / Public Instance](https://app.screego.net/) ᛫ [Installation](https://screego.net/#/install) ᛫ [Configuration](https://screego.net/#/config) ## Versioning We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/screego/server/tags). ================================================ FILE: auth/auth.go ================================================ package auth import ( "encoding/csv" "encoding/json" "errors" "io" "net/http" "os" "github.com/gorilla/sessions" "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" ) type Users struct { Lookup map[string]string store sessions.Store sessionTimeout int } type UserPW struct { Name string Pass string } func read(r io.Reader) ([]UserPW, error) { reader := csv.NewReader(r) reader.Comma = ':' reader.Comment = '#' reader.TrimLeadingSpace = true records, err := reader.ReadAll() if err != nil { return nil, err } result := []UserPW{} for _, record := range records { if len(record) != 2 { return nil, errors.New("malformed users file") } result = append(result, UserPW{Name: record[0], Pass: record[1]}) } return result, nil } func ReadPasswordsFile(path string, secret []byte, sessionTimeout int) (*Users, error) { users := &Users{ Lookup: map[string]string{}, sessionTimeout: sessionTimeout, store: sessions.NewCookieStore(secret), } if path == "" { log.Info().Msg("Users file not specified") return users, nil } file, err := os.Open(path) if err != nil { return users, err } defer file.Close() userPws, err := read(file) if err != nil { return users, err } for _, record := range userPws { users.Lookup[record.Name] = record.Pass } log.Info().Int("amount", len(users.Lookup)).Msg("Loaded Users") return users, nil } type Response struct { Message string `json:"message"` } func (u *Users) CurrentUser(r *http.Request) (string, bool) { s, _ := u.store.Get(r, "user") user, ok := s.Values["user"].(string) if !ok { return "guest", ok } return user, ok } func (u *Users) Logout(w http.ResponseWriter, r *http.Request) { session := sessions.NewSession(u.store, "user") session.IsNew = true if err := u.store.Save(r, w, session); err != nil { w.WriteHeader(500) _ = json.NewEncoder(w).Encode(&Response{ Message: err.Error(), }) return } w.WriteHeader(200) } func (u *Users) Authenticate(w http.ResponseWriter, r *http.Request) { user := r.FormValue("user") pass := r.FormValue("pass") if !u.Validate(user, pass) { w.WriteHeader(401) _ = json.NewEncoder(w).Encode(&Response{ Message: "could not authenticate", }) return } session := sessions.NewSession(u.store, "user") session.IsNew = true session.Options.MaxAge = u.sessionTimeout session.Values["user"] = user if err := u.store.Save(r, w, session); err != nil { w.WriteHeader(500) _ = json.NewEncoder(w).Encode(&Response{ Message: err.Error(), }) return } w.WriteHeader(200) _ = json.NewEncoder(w).Encode(&Response{ Message: "authenticated", }) } func (u Users) Validate(user, password string) bool { realPassword, exists := u.Lookup[user] return exists && bcrypt.CompareHashAndPassword([]byte(realPassword), []byte(password)) == nil } ================================================ FILE: cmd/command.go ================================================ package cmd import ( "fmt" "os" "github.com/rs/zerolog/log" "github.com/urfave/cli" ) func Run(version, commitHash string) { app := cli.App{ Name: "screego", Version: fmt.Sprintf("%s; screego/server@%s", version, commitHash), Commands: []cli.Command{ serveCmd(version), hashCmd, }, } err := app.Run(os.Args) if err != nil { log.Fatal().Err(err).Msg("app error") } } ================================================ FILE: cmd/hash.go ================================================ package cmd import ( "fmt" "os" "syscall" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/screego/server/logger" "github.com/urfave/cli" "golang.org/x/crypto/bcrypt" "golang.org/x/term" ) var hashCmd = cli.Command{ Name: "hash", Flags: []cli.Flag{ &cli.StringFlag{Name: "name"}, &cli.StringFlag{Name: "pass"}, }, Action: func(ctx *cli.Context) { logger.Init(zerolog.ErrorLevel) name := ctx.String("name") pass := []byte(ctx.String("pass")) if name == "" { log.Fatal().Msg("--name must be set") } if len(pass) == 0 { var err error _, _ = fmt.Fprint(os.Stderr, "Enter Password: ") pass, err = term.ReadPassword(int(syscall.Stdin)) if err != nil { log.Fatal().Err(err).Msg("could not read stdin") } _, _ = fmt.Fprintln(os.Stderr, "") } hashedPw, err := bcrypt.GenerateFromPassword(pass, 12) if err != nil { log.Fatal().Err(err).Msg("could not generate password") } fmt.Printf("%s:%s", name, string(hashedPw)) fmt.Println("") }, } ================================================ FILE: cmd/serve.go ================================================ package cmd import ( "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/screego/server/auth" "github.com/screego/server/config" "github.com/screego/server/logger" "github.com/screego/server/router" "github.com/screego/server/server" "github.com/screego/server/turn" "github.com/screego/server/ws" "github.com/urfave/cli" ) func serveCmd(version string) cli.Command { return cli.Command{ Name: "serve", Action: func(ctx *cli.Context) { conf, errs := config.Get() logger.Init(conf.LogLevel.AsZeroLogLevel()) exit := false for _, err := range errs { log.WithLevel(err.Level).Msg(err.Msg) exit = exit || err.Level == zerolog.FatalLevel || err.Level == zerolog.PanicLevel } if exit { os.Exit(1) } if _, _, err := conf.TurnIPProvider.Get(); err != nil { // error is already logged by .Get() os.Exit(1) } users, err := auth.ReadPasswordsFile(conf.UsersFile, conf.Secret, conf.SessionTimeoutSeconds) if err != nil { log.Fatal().Str("file", conf.UsersFile).Err(err).Msg("While loading users file") } tServer, err := turn.Start(conf) if err != nil { log.Fatal().Err(err).Msg("could not start turn server") } rooms := ws.NewRooms(tServer, users, conf) go rooms.Start() r := router.Router(conf, rooms, users, version) if err := server.Start(r, conf.ServerAddress, conf.TLSCertFile, conf.TLSKeyFile); err != nil { log.Fatal().Err(err).Msg("http server") } }, } } ================================================ FILE: config/config.go ================================================ package config import ( "crypto/rand" "errors" "fmt" "net" "os" "path/filepath" "regexp" "strconv" "strings" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" "github.com/rs/zerolog" "github.com/screego/server/config/ipdns" "github.com/screego/server/config/mode" ) var ( prefix = "screego" files = []string{"screego.config.development.local", "screego.config.development", "screego.config.local", "screego.config"} absoluteFiles = []string{"/etc/screego/server.config"} osExecutable = os.Executable osStat = os.Stat ) const ( AuthModeTurn = "turn" AuthModeAll = "all" AuthModeNone = "none" ) // Config represents the application configuration. type Config struct { LogLevel LogLevel `default:"info" split_words:"true"` ExternalIP []string `split_words:"true"` TLSCertFile string `split_words:"true"` TLSKeyFile string `split_words:"true"` ServerTLS bool `split_words:"true"` ServerAddress string `default:":5050" split_words:"true"` Secret []byte `split_words:"true"` SessionTimeoutSeconds int `default:"0" split_words:"true"` TurnAddress string `default:":3478" required:"true" split_words:"true"` TurnPortRange string `split_words:"true"` TurnExternalIP []string `split_words:"true"` TurnExternalPort string `default:"3478" split_words:"true"` TurnExternalSecret string `split_words:"true"` TrustProxyHeaders bool `split_words:"true"` AuthMode string `default:"turn" split_words:"true"` CorsAllowedOrigins []string `split_words:"true"` UsersFile string `split_words:"true"` Prometheus bool `split_words:"true"` CheckOrigin func(string) bool `ignored:"true" json:"-"` TurnExternal bool `ignored:"true"` TurnIPProvider ipdns.Provider `ignored:"true"` TurnPort string `ignored:"true"` TurnDenyPeers []string `default:"0.0.0.0/8,127.0.0.1/8,::/128,::1/128,fe80::/10" split_words:"true"` TurnDenyPeersParsed []*net.IPNet `ignored:"true"` CloseRoomWhenOwnerLeaves bool `default:"true" split_words:"true"` } func (c Config) parsePortRange() (uint16, uint16, error) { if c.TurnPortRange == "" { return 0, 0, nil } parts := strings.Split(c.TurnPortRange, ":") if len(parts) != 2 { return 0, 0, errors.New("must include one colon") } stringMin := parts[0] stringMax := parts[1] min64, err := strconv.ParseUint(stringMin, 10, 16) if err != nil { return 0, 0, fmt.Errorf("invalid min: %s", err) } max64, err := strconv.ParseUint(stringMax, 10, 16) if err != nil { return 0, 0, fmt.Errorf("invalid max: %s", err) } return uint16(min64), uint16(max64), nil } func (c Config) PortRange() (uint16, uint16, bool) { min, max, _ := c.parsePortRange() return min, max, min != 0 && max != 0 } // Get loads the application config. func Get() (Config, []FutureLog) { var logs []FutureLog dir, log := getExecutableOrWorkDir() if log != nil { logs = append(logs, *log) } for _, file := range getFiles(dir) { _, fileErr := osStat(file) if fileErr == nil { if err := godotenv.Load(file); err != nil { logs = append(logs, futureFatal(fmt.Sprintf("cannot load file %s: %s", file, err))) } else { logs = append(logs, FutureLog{ Level: zerolog.DebugLevel, Msg: fmt.Sprintf("Loading file %s", file), }) } } else if os.IsNotExist(fileErr) { continue } else { logs = append(logs, FutureLog{ Level: zerolog.WarnLevel, Msg: fmt.Sprintf("cannot read file %s because %s", file, fileErr), }) } } config := Config{} err := envconfig.Process(prefix, &config) if err != nil { logs = append(logs, futureFatal(fmt.Sprintf("cannot parse env params: %s", err))) } if config.AuthMode != AuthModeTurn && config.AuthMode != AuthModeAll && config.AuthMode != AuthModeNone { logs = append(logs, futureFatal(fmt.Sprintf("invalid SCREEGO_AUTH_MODE: %s", config.AuthMode))) } if config.ServerTLS { if config.TLSCertFile == "" { logs = append(logs, futureFatal("SCREEGO_TLS_CERT_FILE must be set if TLS is enabled")) } if config.TLSKeyFile == "" { logs = append(logs, futureFatal("SCREEGO_TLS_KEY_FILE must be set if TLS is enabled")) } } var compiledAllowedOrigins []*regexp.Regexp for _, origin := range config.CorsAllowedOrigins { compiled, err := regexp.Compile(origin) if err != nil { logs = append(logs, futureFatal(fmt.Sprintf("invalid regex: %s", err))) } compiledAllowedOrigins = append(compiledAllowedOrigins, compiled) } config.CheckOrigin = func(origin string) bool { if origin == "" { return true } for _, compiledOrigin := range compiledAllowedOrigins { if compiledOrigin.Match([]byte(strings.ToLower(origin))) { return true } } return false } if len(config.Secret) == 0 { config.Secret = make([]byte, 32) if _, err := rand.Read(config.Secret); err == nil { logs = append(logs, FutureLog{ Level: zerolog.InfoLevel, Msg: "SCREEGO_SECRET unset, user logins will be invalidated on restart", }) } else { logs = append(logs, futureFatal(fmt.Sprintf("cannot create secret %s", err))) } } var errs []FutureLog if len(config.TurnExternalIP) > 0 { if len(config.ExternalIP) > 0 { logs = append(logs, futureFatal("SCREEGO_EXTERNAL_IP and SCREEGO_TURN_EXTERNAL_IP must not be both set")) } config.TurnIPProvider, errs = parseIPProvider(config.TurnExternalIP, "SCREEGO_TURN_EXTERNAL_IP") config.TurnPort = config.TurnExternalPort config.TurnExternal = true logs = append(logs, errs...) if config.TurnExternalSecret == "" { logs = append(logs, futureFatal("SCREEGO_TURN_EXTERNAL_SECRET must be set if external TURN server is used")) } } else if len(config.ExternalIP) > 0 { config.TurnIPProvider, errs = parseIPProvider(config.ExternalIP, "SCREEGO_EXTERNAL_IP") logs = append(logs, errs...) split := strings.Split(config.TurnAddress, ":") config.TurnPort = split[len(split)-1] } else { logs = append(logs, futureFatal("SCREEGO_EXTERNAL_IP or SCREEGO_TURN_EXTERNAL_IP must be set")) } min, max, err := config.parsePortRange() if err != nil { logs = append(logs, futureFatal(fmt.Sprintf("invalid SCREEGO_TURN_PORT_RANGE: %s", err))) } else if min == 0 && max == 0 { // valid; no port range } else if min == 0 || max == 0 { logs = append(logs, futureFatal("invalid SCREEGO_TURN_PORT_RANGE: min or max port is 0")) } else if min > max { logs = append(logs, futureFatal(fmt.Sprintf("invalid SCREEGO_TURN_PORT_RANGE: min port (%d) is higher than max port (%d)", min, max))) } else if (max - min) < 40 { logs = append(logs, FutureLog{ Level: zerolog.WarnLevel, Msg: "Less than 40 ports are available for turn. When using multiple TURN connections this may not be enough", }) } logs = append(logs, logDeprecated()...) for _, cidrString := range config.TurnDenyPeers { _, cidr, err := net.ParseCIDR(cidrString) if err != nil { logs = append(logs, FutureLog{ Level: zerolog.FatalLevel, Msg: fmt.Sprintf("Invalid SCREEGO_TURN_DENY_PEERS %q: %s", cidrString, err), }) } else { config.TurnDenyPeersParsed = append(config.TurnDenyPeersParsed, cidr) } } logs = append(logs, FutureLog{ Level: zerolog.InfoLevel, Msg: fmt.Sprintf("Deny turn peers within %q", config.TurnDenyPeersParsed), }) return config, logs } func logDeprecated() []FutureLog { if os.Getenv("SCREEGO_TURN_STRICT_AUTH") != "" { return []FutureLog{{Level: zerolog.WarnLevel, Msg: "The setting SCREEGO_TURN_STRICT_AUTH has been removed."}} } return nil } func getExecutableOrWorkDir() (string, *FutureLog) { dir, err := getExecutableDir() // when using `go run main.go` the executable lives in th temp directory therefore the env.development // will not be read, this enforces that the current work directory is used in dev mode. if err != nil || mode.Get() == mode.Dev { return filepath.Dir("."), err } return dir, nil } func getExecutableDir() (string, *FutureLog) { ex, err := osExecutable() if err != nil { return "", &FutureLog{ Level: zerolog.ErrorLevel, Msg: "Could not get path of executable using working directory instead. " + err.Error(), } } return filepath.Dir(ex), nil } func getFiles(relativeTo string) []string { var result []string for _, file := range files { result = append(result, filepath.Join(relativeTo, file)) } homeDir, err := os.UserHomeDir() if err == nil { result = append(result, filepath.Join(homeDir, ".config/screego/server.config")) } result = append(result, absoluteFiles...) return result } ================================================ FILE: config/error.go ================================================ package config import "github.com/rs/zerolog" // FutureLog is an intermediate type for log messages. It is used before the config was loaded because without loaded // config we do not know the log level, so we log these messages once the config was initialized. type FutureLog struct { Level zerolog.Level Msg string } func futureFatal(msg string) FutureLog { return FutureLog{ Level: zerolog.FatalLevel, Msg: msg, } } ================================================ FILE: config/ip.go ================================================ package config import ( "context" "fmt" "net" "strings" "time" "github.com/screego/server/config/ipdns" ) func parseIPProvider(ips []string, config string) (ipdns.Provider, []FutureLog) { if len(ips) == 0 { panic("must have at least one ip") } first := ips[0] if strings.HasPrefix(first, "dns:") { if len(ips) > 1 { return nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: when dns server is specified, only one value is allowed", config))} } return parseDNS(strings.TrimPrefix(first, "dns:")), nil } return parseStatic(ips, config) } func parseStatic(ips []string, config string) (*ipdns.Static, []FutureLog) { var static ipdns.Static firstV4, errs := applyIPTo(config, ips[0], &static) if errs != nil { return nil, errs } if len(ips) == 1 { return &static, nil } secondV4, errs := applyIPTo(config, ips[1], &static) if errs != nil { return nil, errs } if firstV4 == secondV4 { return nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: the ips must be of different type ipv4/ipv6", config))} } if len(ips) > 2 { return nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: too many ips supplied", config))} } return &static, nil } func applyIPTo(config, ip string, static *ipdns.Static) (bool, []FutureLog) { parsed := net.ParseIP(ip) if parsed == nil || ip == "0.0.0.0" { return false, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: %s", config, ip))} } v4 := parsed.To4() != nil if v4 { static.V4 = parsed } else { static.V6 = parsed } return v4, nil } func parseDNS(dnsString string) *ipdns.DNS { var dns ipdns.DNS parts := strings.SplitN(dnsString, "@", 2) dns.Domain = parts[0] dns.DNS = "system" if len(parts) == 2 { dns.DNS = parts[1] dns.Resolver = &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{Timeout: 10 * time.Second} return d.DialContext(ctx, network, parts[1]) }, } } return &dns } ================================================ FILE: config/ipdns/dns.go ================================================ package ipdns import ( "context" "errors" "net" "strings" "sync" "time" "github.com/rs/zerolog/log" ) type DNS struct { sync.Mutex DNS string Resolver *net.Resolver Domain string refetch time.Time v4 net.IP v6 net.IP err error } func (s *DNS) Get() (net.IP, net.IP, error) { s.Lock() defer s.Unlock() if s.refetch.Before(time.Now()) { oldV4, oldV6 := s.v4, s.v6 s.v4, s.v6, s.err = s.lookup() if s.err == nil { if !oldV4.Equal(s.v4) || !oldV6.Equal(s.v6) { log.Info().Str("v4", s.v4.String()). Str("v6", s.v6.String()). Str("domain", s.Domain). Str("dns", s.DNS). Msg("DNS External IP") } s.refetch = time.Now().Add(time.Minute) } else { // don't spam the dns server s.refetch = time.Now().Add(time.Second) log.Err(s.err).Str("domain", s.Domain).Str("dns", s.DNS).Msg("DNS External IP") } } return s.v4, s.v6, s.err } func (s *DNS) lookup() (net.IP, net.IP, error) { ips, err := s.Resolver.LookupIP(context.Background(), "ip", s.Domain) if err != nil { if dns, ok := err.(*net.DNSError); ok && s.DNS != "system" { dns.Server = "" } return nil, nil, err } var v4, v6 net.IP for _, ip := range ips { isV6 := strings.Contains(ip.String(), ":") if isV6 && v6 == nil { v6 = ip } else if !isV6 && v4 == nil { v4 = ip } } if v4 == nil && v6 == nil { return nil, nil, errors.New("dns record doesn't have an A or AAAA record") } return v4, v6, nil } ================================================ FILE: config/ipdns/provider.go ================================================ package ipdns import "net" type Provider interface { Get() (net.IP, net.IP, error) } ================================================ FILE: config/ipdns/static.go ================================================ package ipdns import "net" type Static struct { V4 net.IP V6 net.IP } func (s *Static) Get() (net.IP, net.IP, error) { return s.V4, s.V6, nil } ================================================ FILE: config/loglevel.go ================================================ package config import ( "errors" "github.com/rs/zerolog" ) // LogLevel type that provides helper methods for decoding. type LogLevel zerolog.Level // Decode decodes a string to a log level. func (ll *LogLevel) Decode(value string) error { if level, err := zerolog.ParseLevel(value); err == nil { *ll = LogLevel(level) return nil } *ll = LogLevel(zerolog.InfoLevel) return errors.New("unknown log level") } // AsZeroLogLevel converts the LogLevel to a zerolog.Level. func (ll LogLevel) AsZeroLogLevel() zerolog.Level { return zerolog.Level(ll) } ================================================ FILE: config/loglevel_test.go ================================================ package config import ( "testing" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) func TestLogLevel_Decode_success(t *testing.T) { ll := new(LogLevel) err := ll.Decode("fatal") assert.Nil(t, err) assert.Equal(t, ll.AsZeroLogLevel(), zerolog.FatalLevel) } func TestLogLevel_Decode_fail(t *testing.T) { ll := new(LogLevel) err := ll.Decode("asdasdasdasdasdasd") assert.EqualError(t, err, "unknown log level") assert.Equal(t, ll.AsZeroLogLevel(), zerolog.InfoLevel) } ================================================ FILE: config/mode/mode.go ================================================ package mode const ( // Dev for development mode. Dev = "dev" // Prod for production mode. Prod = "prod" ) var mode = Dev // Set sets the new mode. func Set(newMode string) { mode = newMode } // Get returns the current mode. func Get() string { return mode } ================================================ FILE: config/mode/mode_test.go ================================================ package mode import ( "testing" "github.com/stretchr/testify/require" ) func TestGet(t *testing.T) { mode = Prod require.Equal(t, Prod, Get()) } func TestSet(t *testing.T) { Set(Prod) require.Equal(t, Prod, mode) } ================================================ FILE: docs/.nojekyll ================================================ ================================================ FILE: docs/CNAME ================================================ screego.net ================================================ FILE: docs/README.md ================================================ # screego/server In the past I've had some problems sharing my screen with coworkers using corporate chatting solutions like Microsoft Teams. I wanted to show them some of my code, but either the stream lagged several seconds behind or the quality was so poor that my colleagues couldn't read the code. Or both. That's why I created screego. It allows you to share your screen with good quality and low latency. Screego is an addition to existing software and only helps to share your screen. Nothing else (:. ## Features * Multi User Screenshare * Secure transfer via WebRTC * Low latency / High resolution * Simple Install via [Docker](https://hub.docker.com/r/screego/server) / single binary * Integrated [TURN](nat-traversal.md) Server see [NAT Traversal](nat-traversal.md) --- [Demo / Public Instance](https://app.screego.net/) ᛫ [Installation](https://screego.net/#/install) ᛫ [Configuration](https://screego.net/#/config) ================================================ FILE: docs/_sidebar.md ================================================ * [Home](/) * [Installation](install.md) * [Config](config.md) * [NAT Traversal](nat-traversal.md) * [Reverse Proxy](proxy.md) * [Development](development.md) * [FAQ](faq.md) * [GitHub](https://github.com/screego/server) ================================================ FILE: docs/config.md ================================================ # Config !> TLS is required for Screego to work. Either enable TLS inside Screego or use a reverse proxy to serve Screego via TLS. Screego tries to obtain config values from different locations in sequence. Properties will never be overridden. Thus, the first occurrence of a setting will be used. #### Order * Environment Variables * `screego.config.local` (in same path as the binary) * `screego.config` (in same path as the binary) * `$HOME/.config/screego/server.config` * `/etc/screego/server.config` #### Config Example [screego.config.example](https://raw.githubusercontent.com/screego/server/master/screego.config.example ':include :type=code ini') ================================================ FILE: docs/development.md ================================================ # Development Screego requires: - Go 1.15+ - Node 13.x - Yarn 9+ ## Setup ### Clone Repository Clone screego/server source from git: ```bash $ git clone https://github.com/screego/server.git && cd server ``` ### GOPATH If you are in GOPATH, enable [go modules](https://github.com/golang/go/wiki/Modules) explicitly: ```bash $ export GO111MODULE=on ``` ### Download Dependencies: ```bash # Server $ go mod download # UI $ (cd ui && yarn install) ``` ## Start / Linting ### Backend Create a file named `screego.config.development.local` inside the screego folder with the content: ```ini SCREEGO_EXTERNAL_IP=YOURIP ``` and replace `YOURIP` with your external ip. Start the server in development mode. ```bash $ go run . serve ``` The backend is available on [http://localhost:5050](http://localhost:5050) ?> When accessing `localhost:5050` it is normal that there are panics with `no such file or directory`. The UI will be started separately. ### Frontend Start the UI development server. _Commands must be executed inside the ui directory._ ```bash $ yarn start ``` Open [http://localhost:3000](http://localhost:3000) inside your favorite browser. ### Lint Screego uses [golangci-lint](https://github.com/golangci/golangci-lint) for linting. After installation you can check the source code with: ```bash $ golangci-lint run ``` ## Build 1. [Setup](#setup) 1. Build the UI ```bash $ (cd ui && yarn build) ``` 1. Build the binary ```bash go build -ldflags "-X main.version=$(git describe --tags HEAD) -X main.mode=prod" -o screego ./main.go ``` ================================================ FILE: docs/faq.md ================================================ # Frequently Asked Questions ## Stream doesn't load Check that * you are using https to access Screego. * `SCREEGO_EXTERNAL_IP` is set to your external IP. See [Configuration](config.md) * you are using TURN for NAT-Traversal. See [NAT-Traversal](nat-traversal.md). *On app.screego.net it's enabled without login; when self-hosting it requires user login* * your browser doesn't block WebRTC (extensions or other settings) * you have opened ports in your firewall. By default 5050, 3478 and any UDP port when using TURN. ## Automatically create room on join Sometimes you want to reuse the screego room, but always have to recreate it. By passing `create=true` in the url, you can automatically create the room if it does not exist. Example: https://app.screego.net/?room=not-existing-room&create=true ================================================ FILE: docs/index.html ================================================!> Screego may not work correctly when deploying it in docker without `network_mode: host`. See [#226](https://github.com/screego/server/issues/226) ```bash $ docker run -it \ -e SCREEGO_EXTERNAL_IP=EXTERNALIP \ -e SCREEGO_TURN_PORT_RANGE=50000:50200 \ -p 5050:5050 \ -p 3478:3478 \ -p 50000-50200:50000-50200/udp \ screego/server:GITHUB_VERSION ``` #### docker-compose.yml ```yml version: "3.7" services: screego: image: ghcr.io/screego/server:GITHUB_VERSION ports: - 5050:5050 - 3478:3478 - 50000-50200:50000-50200/udp environment: SCREEGO_EXTERNAL_IP: "192.168.178.2" SCREEGO_TURN_PORT_RANGE: "50000:50200" ```