// // rhimportd // // The Radio Helsinki Rivendell Import Daemon // // // Copyright (C) 2015-2016 Christian Pointner // // This file is part of rhimportd. // // rhimportd is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // rhimportd is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with rhimportd. If not, see . // package rhimport import ( "crypto/rand" "encoding/base64" "fmt" "io/ioutil" "log" "net/http" "strings" "time" "code.helsinki.at/rhrd-go/rddb" ) type newSessionResponse struct { id string session *Session responsecode int errorstring string } type newSessionRequest struct { ctx *Context refId string response chan newSessionResponse } type getSessionResponse struct { session *Session refId string responsecode int errorstring string } type getSessionRequest struct { user string id string refId string response chan getSessionResponse } type SessionsUpdateCB func(added, removed map[string]string, userdata interface{}) bool type SessionsListCB struct { cb SessionsUpdateCB userdata interface{} } type listSessionsResponse struct { sessions map[string]string responsecode int errorstring string } type listSessionsRequest struct { user string password string trusted bool callback SessionsUpdateCB userdata interface{} response chan listSessionsResponse } type removeSessionResponse struct { responsecode int errorstring string } type removeSessionRequest struct { user string id string response chan removeSessionResponse } type sessionStoreSessionElement struct { s *session refId string } type sessionStoreUserElement struct { sessions map[string]*sessionStoreSessionElement updateCBs []SessionsListCB } func (user *sessionStoreUserElement) callUpdateHandler(added, removed map[string]string) { var keptCBs []SessionsListCB for _, cb := range user.updateCBs { if cb.cb != nil { if keep := cb.cb(added, removed, cb.userdata); keep { keptCBs = append(keptCBs, cb) } } } user.updateCBs = keptCBs } func (user *sessionStoreUserElement) callUpdateHandlerAdd(id, refId string) { added := make(map[string]string) added[id] = refId user.callUpdateHandler(added, nil) } func (user *sessionStoreUserElement) callUpdateHandlerRemove(id, refId string) { removed := make(map[string]string) removed[id] = refId user.callUpdateHandler(nil, removed) } type sessionStore struct { store map[string]*sessionStoreUserElement conf *Config db *rddb.DB stdlog *log.Logger dbglog *log.Logger quit chan bool done chan bool newChan chan newSessionRequest getChan chan getSessionRequest listChan chan listSessionsRequest removeChan chan removeSessionRequest } func generateSessionId() (string, error) { var b [32]byte if _, err := rand.Read(b[:]); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b[:]), nil } func (store *sessionStore) new(ctx *Context, refId string) (resp newSessionResponse) { resp.responsecode = http.StatusOK resp.errorstring = "OK" if !ctx.Trusted { if ok, err := store.db.CheckPassword(ctx.UserName, ctx.Password); err != nil { resp.responsecode = http.StatusInternalServerError resp.errorstring = err.Error() return } else if !ok { resp.responsecode = http.StatusUnauthorized resp.errorstring = "invalid username and/or password" return } } id, err := generateSessionId() if err != nil { resp.responsecode = http.StatusInternalServerError resp.errorstring = err.Error() return } resp.id = id if _, exists := store.store[ctx.UserName]; !exists { newuser := &sessionStoreUserElement{} newuser.sessions = make(map[string]*sessionStoreSessionElement) store.store[ctx.UserName] = newuser } ctx.conf = store.conf ctx.db = store.db if pref := ctx.stdlog.Prefix(); strings.Contains(pref, "%s") { ctx.stdlog.SetPrefix(fmt.Sprintf(pref, resp.id)) } if pref := ctx.dbglog.Prefix(); strings.Contains(pref, "%s") { ctx.dbglog.SetPrefix(fmt.Sprintf(pref, resp.id)) } s := &sessionStoreSessionElement{newSession(ctx, func() { store.GetInterface().Remove(ctx.UserName, resp.id) }), refId} store.store[ctx.UserName].sessions[resp.id] = s resp.session = store.store[ctx.UserName].sessions[resp.id].s.getInterface() store.dbglog.Printf("SessionStore: created session for '%s' -> %s", ctx.UserName, resp.id) store.store[ctx.UserName].callUpdateHandlerAdd(resp.id, refId) return } func (store *sessionStore) get(username, id string) (resp getSessionResponse) { resp.responsecode = http.StatusOK resp.errorstring = "OK" user, exists := store.store[username] if !exists { resp.responsecode = http.StatusNotFound resp.errorstring = fmt.Sprintf("SessionStore: session '%s/%s' not found", username, id) return } if session, exists := user.sessions[id]; exists { resp.session = session.s.getInterface() resp.refId = session.refId return } resp.responsecode = http.StatusNotFound resp.errorstring = fmt.Sprintf("SessionStore: session '%s/%s' not found", username, id) return } func (store *sessionStore) list(username, password string, trusted bool, userdata interface{}, cb SessionsUpdateCB) (resp listSessionsResponse) { resp.responsecode = http.StatusOK resp.errorstring = "OK" if !trusted { if ok, err := store.db.CheckPassword(username, password); err != nil { resp.responsecode = http.StatusInternalServerError resp.errorstring = err.Error() return } else if !ok { resp.responsecode = http.StatusUnauthorized resp.errorstring = "invalid username and/or password" return } } resp.sessions = make(map[string]string) if user, exists := store.store[username]; exists { for id, e := range user.sessions { resp.sessions[id] = e.refId } if cb != nil { user.updateCBs = append(user.updateCBs, SessionsListCB{cb, userdata}) } } else if cb != nil { newuser := &sessionStoreUserElement{} newuser.sessions = make(map[string]*sessionStoreSessionElement) newuser.updateCBs = []SessionsListCB{SessionsListCB{cb, userdata}} store.store[username] = newuser } return } func (store *sessionStore) remove(username, id string) (resp removeSessionResponse) { resp.responsecode = http.StatusOK resp.errorstring = "OK" user, exists := store.store[username] if !exists { resp.responsecode = http.StatusNotFound resp.errorstring = fmt.Sprintf("SessionStore: session '%s/%s' not found", username, id) return } if session, exists := user.sessions[id]; exists { go session.s.cleanup() // cleanup could take a while -> don't block all the other stuff refId := session.refId delete(user.sessions, id) store.dbglog.Printf("SessionStore: removed session '%s/%s'", username, id) user.callUpdateHandlerRemove(id, refId) if len(user.sessions) == 0 && len(user.updateCBs) == 0 { delete(store.store, username) store.dbglog.Printf("SessionStore: removed user '%s'", username) } } else { resp.responsecode = http.StatusNotFound resp.errorstring = fmt.Sprintf("SessionStore: session '%s/%s' not found", username, id) } return } func (store *sessionStore) maintenanceTask() { for name, user := range store.store { user.callUpdateHandler(nil, nil) if len(user.sessions) == 0 && len(user.updateCBs) == 0 { delete(store.store, name) store.dbglog.Printf("SessionStore: removed user '%s'", name) } } } func (store *sessionStore) dispatchRequests() { defer func() { store.done <- true }() mt := time.NewTicker(1 * time.Minute) for { select { case <-store.quit: return case <-mt.C: store.maintenanceTask() case req := <-store.newChan: req.response <- store.new(req.ctx, req.refId) case req := <-store.getChan: req.response <- store.get(req.user, req.id) case req := <-store.listChan: req.response <- store.list(req.user, req.password, req.trusted, req.userdata, req.callback) case req := <-store.removeChan: req.response <- store.remove(req.user, req.id) } } } // ********************************************************* // Public Interface type SessionStore struct { newChan chan<- newSessionRequest getChan chan<- getSessionRequest listChan chan listSessionsRequest removeChan chan<- removeSessionRequest } func (store *SessionStore) New(ctx *Context, refId string) (string, *Session, int, string) { resCh := make(chan newSessionResponse) req := newSessionRequest{} req.ctx = ctx req.refId = refId req.response = resCh store.newChan <- req res := <-resCh return res.id, res.session, res.responsecode, res.errorstring } func (store *SessionStore) Get(user, id string) (*Session, string, int, string) { resCh := make(chan getSessionResponse) req := getSessionRequest{} req.user = user req.id = id req.response = resCh store.getChan <- req res := <-resCh return res.session, res.refId, res.responsecode, res.errorstring } func (store *SessionStore) List(user, password string, trusted bool, userdata interface{}, cb SessionsUpdateCB) (map[string]string, int, string) { resCh := make(chan listSessionsResponse) req := listSessionsRequest{} req.user = user req.password = password req.trusted = trusted req.response = resCh req.callback = cb req.userdata = userdata store.listChan <- req res := <-resCh return res.sessions, res.responsecode, res.errorstring } func (store *SessionStore) Remove(user, id string) (int, string) { resCh := make(chan removeSessionResponse) req := removeSessionRequest{} req.user = user req.id = id req.response = resCh store.removeChan <- req res := <-resCh return res.responsecode, res.errorstring } func (store *sessionStore) GetInterface() *SessionStore { ch := &SessionStore{} ch.newChan = store.newChan ch.getChan = store.getChan ch.listChan = store.listChan ch.removeChan = store.removeChan return ch } func (store *sessionStore) Cleanup() { store.quit <- true <-store.done close(store.quit) close(store.done) close(store.newChan) close(store.getChan) close(store.listChan) close(store.removeChan) } func NewSessionStore(conf *Config, db *rddb.DB, stdlog, dbglog *log.Logger) (store *sessionStore, err error) { if stdlog == nil { stdlog = log.New(ioutil.Discard, "", 0) } if dbglog == nil { dbglog = log.New(ioutil.Discard, "", 0) } store = &sessionStore{} store.conf = conf store.db = db store.stdlog = stdlog store.dbglog = dbglog store.quit = make(chan bool, 1) store.done = make(chan bool) store.store = make(map[string]*sessionStoreUserElement) store.newChan = make(chan newSessionRequest, 10) store.getChan = make(chan getSessionRequest, 10) store.listChan = make(chan listSessionsRequest, 10) store.removeChan = make(chan removeSessionRequest, 10) go store.dispatchRequests() return }