Set long cache-control and add a hash to css and js urls, clean some things up
This commit is contained in:
parent
2ccca3a778
commit
df02d27674
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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") {
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user