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()
+}