Set long cache-control and add a hash to css and js urls, clean some things up

This commit is contained in:
Ken-Håvard Lieng 2016-01-25 06:01:40 +01:00
parent 2ccca3a778
commit df02d27674
9 changed files with 171 additions and 77 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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) {

View File

@ -8,8 +8,10 @@ import (
)
var (
index_start = []byte(`<!DOCTYPE html><html lang=en><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Dispatch</title><link href="https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto+Mono:400,700" rel=stylesheet><link href=/bundle.css rel=stylesheet></head><body><div id=root></div><script>window.__ENV__=`)
index_end = []byte(`;</script><script src=/bundle.js></script></body></html>`)
index_0 = []byte(`<!DOCTYPE html><html lang=en><head><meta charset=UTF-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Dispatch</title><link href="https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto+Mono:400,700" rel=stylesheet><link href=/`)
index_1 = []byte(` rel=stylesheet></head><body><div id=root></div><script>window.__ENV__=`)
index_2 = []byte(`;</script><script src=/`)
index_3 = []byte(`></script></body></html>`)
)
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)
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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
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") {

View File

@ -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() {

View File

@ -19,7 +19,7 @@ type Session struct {
ws map[string]*wsConn
wsLock sync.Mutex
out chan WSResponse
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()
}