diff --git a/README.md b/README.md index 84770ded..13c8c786 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,10 @@ gulp build The server needs to be rebuilt after this. -For development with hot reloading enabled just run: +For development with hot reloading enabled run: ```bash gulp +dispatch --dev ``` ## Libraries diff --git a/commands/dispatch.go b/commands/dispatch.go index 528d7d2f..86e79a05 100644 --- a/commands/dispatch.go +++ b/commands/dispatch.go @@ -45,6 +45,8 @@ var rootCmd = &cobra.Command{ log.Println("Storing data at", storage.Path.Root()) storage.Open() + defer storage.Close() + server.Run() }, } @@ -57,11 +59,13 @@ func init() { rootCmd.AddCommand(clearCmd) rootCmd.AddCommand(configCmd) - rootCmd.Flags().IntP("port", "p", 80, "port to listen on") rootCmd.PersistentFlags().String("dir", storage.DefaultDirectory(), "directory to store config and data in") + rootCmd.Flags().IntP("port", "p", 80, "port to listen on") + rootCmd.Flags().Bool("dev", false, "development mode") - viper.BindPFlag("port", rootCmd.Flags().Lookup("port")) viper.BindPFlag("dir", rootCmd.PersistentFlags().Lookup("dir")) + viper.BindPFlag("port", rootCmd.Flags().Lookup("port")) + viper.BindPFlag("dev", rootCmd.Flags().Lookup("dev")) viper.SetDefault("verify_client_certificates", true) } diff --git a/server/auth.go b/server/auth.go index 6ba40e63..76557615 100644 --- a/server/auth.go +++ b/server/auth.go @@ -13,6 +13,37 @@ import ( "github.com/khlieng/dispatch/storage" ) +const ( + cookieName = "dispatch" +) + +var ( + hmacKey []byte +) + +func initAuth() { + var err error + hmacKey, err = getHMACKey() + if err != nil { + log.Fatal(err) + } +} + +func getHMACKey() ([]byte, error) { + key, err := ioutil.ReadFile(storage.Path.HMACKey()) + if err != nil { + key = make([]byte, 32) + rand.Read(key) + + err = ioutil.WriteFile(storage.Path.HMACKey(), key, 0600) + if err != nil { + return nil, err + } + } + + return key, nil +} + func handleAuth(w http.ResponseWriter, r *http.Request) *Session { var session *Session @@ -21,22 +52,14 @@ func handleAuth(w http.ResponseWriter, r *http.Request) *Session { authLog(r, "No cookie set") session = newUser(w, r) } else { - token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return hmacKey, nil - }) + token, err := parseToken(cookie.Value) if err == nil && token.Valid { userID := uint64(token.Claims["UserID"].(float64)) log.Println(r.RemoteAddr, "[Auth] GET", r.URL.Path, "| Valid token | User ID:", userID) - sessionLock.Lock() - session = sessions[userID] - sessionLock.Unlock() - + session = sessions.get(userID) if session == nil { // A previous anonymous session has been cleaned up, create a new one session = newUser(w, r) @@ -47,6 +70,7 @@ func handleAuth(w http.ResponseWriter, r *http.Request) *Session { } else { authLog(r, "Invalid token") } + session = newUser(w, r) } } @@ -63,11 +87,7 @@ func newUser(w http.ResponseWriter, r *http.Request) *Session { log.Println(r.RemoteAddr, "[Auth] Create session | User ID:", user.ID) session := NewSession(user) - - sessionLock.Lock() - sessions[user.ID] = session - sessionLock.Unlock() - + sessions.set(user.ID, session) go session.run() token := jwt.New(jwt.SigningMethodHS256) @@ -89,19 +109,14 @@ func newUser(w http.ResponseWriter, r *http.Request) *Session { return session } -func getHMACKey() ([]byte, error) { - key, err := ioutil.ReadFile(storage.Path.HMACKey()) - if err != nil { - key = make([]byte, 32) - rand.Read(key) - - err = ioutil.WriteFile(storage.Path.HMACKey(), key, 0600) - if err != nil { - return nil, err +func parseToken(cookie string) (*jwt.Token, error) { + return jwt.Parse(cookie, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - } + return hmacKey, nil + }) - return key, nil } func authLog(r *http.Request, s string) { diff --git a/server/index_template.go b/server/index_template.go index 82e3fbb7..4479b8ed 100644 --- a/server/index_template.go +++ b/server/index_template.go @@ -8,8 +8,10 @@ import ( ) var ( - index_start = []byte(`Dispatch
`) + index_0 = []byte(`Dispatch
`) ) type connectDefaults struct { @@ -25,7 +27,9 @@ type indexData struct { } func renderIndex(w io.Writer, session *Session) { - w.Write(index_start) + w.Write(index_0) + w.Write([]byte(files[1].Path)) + w.Write(index_1) json.NewEncoder(w).Encode(indexData{ Defaults: connectDefaults{ @@ -37,5 +41,7 @@ func renderIndex(w io.Writer, session *Session) { }, }) - w.Write(index_end) + w.Write(index_2) + w.Write([]byte(files[0].Path)) + w.Write(index_3) } diff --git a/server/irc.go b/server/irc.go index 63254b58..757ff632 100644 --- a/server/irc.go +++ b/server/irc.go @@ -13,7 +13,7 @@ import ( func reconnectIRC() { for _, user := range storage.LoadUsers() { session := NewSession(user) - sessions[user.ID] = session + sessions.set(user.ID, session) go session.run() channels := user.GetChannels() diff --git a/server/irc_handler_test.go b/server/irc_handler_test.go index ba1bebc5..98c662fa 100644 --- a/server/irc_handler_test.go +++ b/server/irc_handler_test.go @@ -41,7 +41,7 @@ func dispatchMessage(msg *irc.Message) WSResponse { newIRCHandler(c, s).dispatchMessage(msg) - return <-s.out + return <-s.broadcast } func checkResponse(t *testing.T, expectedType string, expectedData interface{}, res WSResponse) { @@ -194,7 +194,7 @@ func TestHandleIRCWhois(t *testing.T) { Realname: "realname", Server: "srv.com", Channels: []string{"#chan", "#chan1"}, - }, <-s.out) + }, <-s.broadcast) } func TestHandleIRCTopic(t *testing.T) { @@ -236,7 +236,7 @@ func TestHandleIRCNames(t *testing.T) { Server: "host.com", Channel: "#chan", Users: []string{"a", "b", "c", "d"}, - }, <-s.out) + }, <-s.broadcast) } func TestHandleIRCMotd(t *testing.T) { @@ -263,5 +263,5 @@ func TestHandleIRCMotd(t *testing.T) { Server: "host.com", Title: "motd title", Content: []string{"line 1", "line 2"}, - }, <-s.out) + }, <-s.broadcast) } diff --git a/server/serve_files.go b/server/serve_files.go index ea8de30f..2a7b2279 100644 --- a/server/serve_files.go +++ b/server/serve_files.go @@ -3,6 +3,8 @@ package server import ( "bytes" "compress/gzip" + "crypto/md5" + "encoding/base64" "io/ioutil" "log" "net/http" @@ -10,22 +12,71 @@ import ( "strings" "time" + "github.com/khlieng/dispatch/Godeps/_workspace/src/github.com/spf13/viper" + "github.com/khlieng/dispatch/assets" ) var files = []File{ - File{"vendor.js", "text/javascript"}, - File{"bundle.js", "text/javascript"}, - File{"bundle.css", "text/css"}, - File{"font/fontello.woff", "application/font-woff"}, - File{"font/fontello.ttf", "application/x-font-ttf"}, - File{"font/fontello.eot", "application/vnd.ms-fontobject"}, - File{"font/fontello.svg", "image/svg+xml"}, + File{ + Path: "bundle.js", + Asset: "bundle.js.gz", + ContentType: "text/javascript", + CacheControl: "max-age=31536000", + }, + File{ + Path: "bundle.css", + Asset: "bundle.css.gz", + ContentType: "text/css", + CacheControl: "max-age=31536000", + }, + File{ + Path: "font/fontello.woff", + Asset: "font/fontello.woff.gz", + ContentType: "application/font-woff", + }, + File{ + Path: "font/fontello.ttf", + Asset: "font/fontello.ttf.gz", + ContentType: "application/x-font-ttf", + }, + File{ + Path: "font/fontello.eot", + Asset: "font/fontello.eot.gz", + ContentType: "application/vnd.ms-fontobject", + }, + File{ + Path: "font/fontello.svg", + Asset: "font/fontello.svg.gz", + ContentType: "image/svg+xml", + }, } type File struct { - Path string - ContentType string + Path string + Asset string + ContentType string + CacheControl string +} + +func initFileServer() { + if !viper.GetBool("dev") { + data, err := assets.Asset(files[0].Asset) + if err != nil { + log.Fatal(err) + } + + hash := md5.Sum(data) + files[0].Path = "bundle." + base64.RawURLEncoding.EncodeToString(hash[:]) + ".js" + + data, err = assets.Asset(files[1].Asset) + if err != nil { + log.Fatal(err) + } + + hash = md5.Sum(data) + files[1].Path = "bundle." + base64.RawURLEncoding.EncodeToString(hash[:]) + ".css" + } } func serveFiles(w http.ResponseWriter, r *http.Request) { @@ -41,7 +92,11 @@ func serveFiles(w http.ResponseWriter, r *http.Request) { for _, file := range files { if strings.HasSuffix(r.URL.Path, file.Path) { - serveFile(w, r, file.Path+".gz", file.ContentType) + if file.CacheControl != "" { + w.Header().Set("Cache-Control", file.CacheControl) + } + + serveFile(w, r, file.Asset, file.ContentType) return } } @@ -57,6 +112,7 @@ func serveIndex(w http.ResponseWriter, r *http.Request) { return } + w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/html") if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { diff --git a/server/server.go b/server/server.go index 1adf235b..3e330908 100644 --- a/server/server.go +++ b/server/server.go @@ -7,7 +7,6 @@ import ( "net/http/httputil" "net/url" "strings" - "sync" "github.com/khlieng/dispatch/Godeps/_workspace/src/github.com/gorilla/websocket" "github.com/khlieng/dispatch/Godeps/_workspace/src/github.com/spf13/viper" @@ -17,16 +16,9 @@ import ( "github.com/khlieng/dispatch/storage" ) -const ( - cookieName = "dispatch" -) - var ( + sessions *sessionStore channelStore *storage.ChannelStore - sessions map[uint64]*Session - sessionLock sync.Mutex - - hmacKey []byte upgrader = websocket.Upgrader{ ReadBufferSize: 1024, @@ -38,21 +30,13 @@ var ( ) func Run() { - defer storage.Close() - + sessions = newSessionStore() channelStore = storage.NewChannelStore() - sessions = make(map[uint64]*Session) - - var err error - hmacKey, err = getHMACKey() - if err != nil { - log.Fatal(err) - } reconnectIRC() + initAuth() + initFileServer() startHTTP() - - select {} } func startHTTP() { diff --git a/server/session.go b/server/session.go index 0ff79d57..0072788f 100644 --- a/server/session.go +++ b/server/session.go @@ -17,9 +17,9 @@ type Session struct { connectionState map[string]bool ircLock sync.Mutex - ws map[string]*wsConn - wsLock sync.Mutex - out chan WSResponse + ws map[string]*wsConn + wsLock sync.Mutex + broadcast chan WSResponse user *storage.User expiration *time.Timer @@ -31,7 +31,7 @@ func NewSession(user *storage.User) *Session { irc: make(map[string]*irc.Client), connectionState: make(map[string]bool), ws: make(map[string]*wsConn), - out: make(chan WSResponse, 32), + broadcast: make(chan WSResponse, 32), user: user, expiration: time.NewTimer(AnonymousSessionExpiration), reset: make(chan time.Duration, 1), @@ -115,7 +115,7 @@ func (s *Session) numWS() int { } func (s *Session) sendJSON(t string, v interface{}) { - s.out <- WSResponse{t, v} + s.broadcast <- WSResponse{t, v} } func (s *Session) sendError(err error, server string) { @@ -134,7 +134,7 @@ func (s *Session) resetExpirationIfEmpty() { func (s *Session) run() { for { select { - case res := <-s.out: + case res := <-s.broadcast: s.wsLock.Lock() for _, ws := range s.ws { ws.out <- res @@ -142,9 +142,7 @@ func (s *Session) run() { s.wsLock.Unlock() case <-s.expiration.C: - sessionLock.Lock() - delete(sessions, s.user.ID) - sessionLock.Unlock() + sessions.delete(s.user.ID) s.user.Remove() return @@ -157,3 +155,33 @@ func (s *Session) run() { } } } + +type sessionStore struct { + sessions map[uint64]*Session + lock sync.Mutex +} + +func newSessionStore() *sessionStore { + return &sessionStore{ + sessions: make(map[uint64]*Session), + } +} + +func (s *sessionStore) get(userid uint64) *Session { + s.lock.Lock() + session := s.sessions[userid] + s.lock.Unlock() + return session +} + +func (s *sessionStore) set(userid uint64, session *Session) { + s.lock.Lock() + s.sessions[userid] = session + s.lock.Unlock() +} + +func (s *sessionStore) delete(userid uint64) { + s.lock.Lock() + delete(s.sessions, userid) + s.lock.Unlock() +}