// // 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" "net/http" "time" "code.helsinki.at/rhrd-go/rddb" ) type newSessionResponse struct { id string session *SessionChan responsecode int errorstring string } type newSessionRequest struct { ctx *Context refId string response chan newSessionResponse } type getSessionResponse struct { session *SessionChan 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.DBChan 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.RawStdEncoding.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 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() rhdl.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) rhdl.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) rhdl.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) rhdl.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 SessionStoreChan struct { newChan chan<- newSessionRequest getChan chan<- getSessionRequest listChan chan listSessionsRequest removeChan chan<- removeSessionRequest } func (store *SessionStoreChan) New(ctx *Context, refId string) (string, *SessionChan, 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 *SessionStoreChan) Get(user, id string) (*SessionChan, 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 *SessionStoreChan) 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 *SessionStoreChan) 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() *SessionStoreChan { ch := &SessionStoreChan{} 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.DBChan) (store *SessionStore, err error) { store = new(SessionStore) store.conf = conf store.db = db 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 }