1package main
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"log"
  7	"math/rand"
  8	"net/http"
  9	"os"
 10	"reflect"
 11	"time"
 12
 13	"github.com/go-chi/chi"
 14	"github.com/go-chi/chi/middleware"
 15	"github.com/gofrs/uuid"
 16	"github.com/jessevdk/go-flags"
 17	"github.com/posener/ctxutil"
 18	"github.com/tomwright/queryparam/v4"
 19	"github.com/zikaeroh/codies/internal/protocol"
 20	"github.com/zikaeroh/codies/internal/server"
 21	"github.com/zikaeroh/codies/internal/version"
 22	"golang.org/x/sync/errgroup"
 23	"nhooyr.io/websocket"
 24)
 25
 26var args = struct {
 27	Addr    string   `long:"addr" env:"CODIES_ADDR" description:"Address to listen at"`
 28	Origins []string `long:"origins" env:"CODIES_ORIGINS" env-delim:"," description:"Additional valid origins for WebSocket connections"`
 29	Debug   bool     `long:"debug" env:"CODIES_DEBUG" description:"Enables debug mode"`
 30}{
 31	Addr: ":5000",
 32}
 33
 34func main() {
 35	rand.Seed(time.Now().Unix())
 36	log.SetFlags(log.LstdFlags | log.Lshortfile)
 37
 38	if _, err := flags.Parse(&args); err != nil {
 39		// Default flag parser prints messages, so just exit.
 40		os.Exit(1)
 41	}
 42
 43	log.Printf("starting codies server, version %s", version.Version())
 44
 45	wsOpts := &websocket.AcceptOptions{
 46		OriginPatterns: args.Origins,
 47	}
 48
 49	if args.Debug {
 50		log.Println("starting in debug mode, allowing any WebSocket origin host")
 51		wsOpts.OriginPatterns = []string{"*"}
 52	}
 53
 54	g, ctx := errgroup.WithContext(ctxutil.Interrupt())
 55
 56	srv := server.NewServer()
 57
 58	r := chi.NewMux()
 59	r.Use(middleware.Heartbeat("/ping"))
 60	r.Use(middleware.Recoverer)
 61	r.NotFound(staticRouter().ServeHTTP)
 62
 63	r.Group(func(r chi.Router) {
 64		r.Use(middleware.NoCache)
 65
 66		r.Get("/api/time", func(w http.ResponseWriter, r *http.Request) {
 67			w.Header().Add("Content-Type", "application/json")
 68			_ = json.NewEncoder(w).Encode(&protocol.TimeResponse{Time: time.Now()})
 69		})
 70
 71		r.Get("/api/exists", func(w http.ResponseWriter, r *http.Request) {
 72			query := &protocol.ExistsQuery{}
 73			if err := queryparam.Parse(r.URL.Query(), query); err != nil {
 74				httpErr(w, http.StatusBadRequest)
 75				return
 76			}
 77
 78			room := srv.FindRoomByID(query.RoomID)
 79			if room == nil {
 80				w.WriteHeader(http.StatusNotFound)
 81			} else {
 82				w.WriteHeader(http.StatusOK)
 83			}
 84
 85			_, _ = w.Write([]byte("."))
 86		})
 87
 88		r.Post("/api/room", func(w http.ResponseWriter, r *http.Request) {
 89			defer r.Body.Close()
 90
 91			req := &protocol.RoomRequest{}
 92			if err := json.NewDecoder(r.Body).Decode(req); err != nil {
 93				httpErr(w, http.StatusBadRequest)
 94				return
 95			}
 96
 97			if !req.Valid() {
 98				httpErr(w, http.StatusBadRequest)
 99				return
100			}
101
102			resp := &protocol.RoomResponse{}
103
104			w.Header().Add("Content-Type", "application/json")
105
106			if req.Create {
107				room, err := srv.CreateRoom(req.RoomName, req.RoomPass)
108				if err != nil {
109					switch err {
110					case server.ErrRoomExists:
111						resp.Error = stringPtr("Room already exists.")
112						w.WriteHeader(http.StatusBadRequest)
113					case server.ErrTooManyRooms:
114						resp.Error = stringPtr("Too many rooms.")
115						w.WriteHeader(http.StatusServiceUnavailable)
116					default:
117						resp.Error = stringPtr("An unknown error occurred.")
118						w.WriteHeader(http.StatusInternalServerError)
119					}
120				} else {
121					resp.ID = &room.ID
122					w.WriteHeader(http.StatusOK)
123				}
124			} else {
125				room := srv.FindRoom(req.RoomName)
126				if room == nil || room.Password != req.RoomPass {
127					resp.Error = stringPtr("Room not found or password does not match.")
128					w.WriteHeader(http.StatusNotFound)
129				} else {
130					resp.ID = &room.ID
131					w.WriteHeader(http.StatusOK)
132				}
133			}
134
135			_ = json.NewEncoder(w).Encode(resp)
136		})
137
138		r.Get("/api/ws", func(w http.ResponseWriter, r *http.Request) {
139			query := &protocol.WSQuery{}
140			if err := queryparam.Parse(r.URL.Query(), query); err != nil {
141				httpErr(w, http.StatusBadRequest)
142				return
143			}
144
145			if !query.Valid() {
146				httpErr(w, http.StatusBadRequest)
147				return
148			}
149
150			room := srv.FindRoomByID(query.RoomID)
151			if room == nil {
152				httpErr(w, http.StatusNotFound)
153				return
154			}
155
156			c, err := websocket.Accept(w, r, wsOpts)
157			if err != nil {
158				log.Println(err)
159				return
160			}
161
162			g.Go(func() error {
163				room.HandleConn(query.PlayerID, query.Nickname, c)
164				return nil
165			})
166		})
167
168		r.Get("/api/stats", func(w http.ResponseWriter, r *http.Request) {
169			rooms, clients := srv.Stats()
170
171			enc := json.NewEncoder(w)
172			enc.SetIndent("", "    ")
173			_ = enc.Encode(&protocol.StatsResponse{
174				Rooms:   rooms,
175				Clients: clients,
176			})
177		})
178	})
179
180	g.Go(func() error {
181		return srv.Run(ctx)
182	})
183
184	httpSrv := http.Server{Addr: args.Addr, Handler: r}
185
186	g.Go(func() error {
187		<-ctx.Done()
188
189		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
190		defer cancel()
191
192		return httpSrv.Shutdown(ctx)
193	})
194
195	g.Go(func() error {
196		return httpSrv.ListenAndServe()
197	})
198
199	log.Fatal(g.Wait())
200}
201
202func staticRouter() http.Handler {
203	fs := http.Dir("./frontend/build")
204	fsh := http.FileServer(fs)
205
206	r := chi.NewMux()
207	r.Use(middleware.Compress(5))
208
209	r.Handle("/static/*", fsh)
210	r.Handle("/favicon/*", fsh)
211
212	r.Group(func(r chi.Router) {
213		r.Use(middleware.NoCache)
214		r.Handle("/*", fsh)
215	})
216
217	return r
218}
219
220func httpErr(w http.ResponseWriter, code int) {
221	http.Error(w, http.StatusText(code), code)
222}
223
224func stringPtr(s string) *string {
225	return &s
226}
227
228func init() {
229	queryparam.DefaultParser.ValueParsers[reflect.TypeOf(uuid.UUID{})] = func(value string, _ string) (reflect.Value, error) {
230		id, err := uuid.FromString(value)
231		return reflect.ValueOf(id), err
232	}
233}