session.go
1 package web 2 3 import ( 4 "encoding/gob" 5 "encoding/hex" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "log" 11 "net/http" 12 "os" 13 "strings" 14 "time" 15 16 dogeboxd "github.com/dogeorg/dogeboxd/pkg" 17 "github.com/gorilla/securecookie" 18 ) 19 20 const sessionExpiry = time.Hour 21 22 type Session struct { 23 Token string 24 Expiration time.Time 25 DKM_TOKEN string 26 } 27 28 var sessions []Session 29 30 func getBearerToken(r *http.Request) (bool, string) { 31 authHeader := r.Header.Get("authorization") 32 33 if authHeader == "" { 34 return false, "" 35 } 36 37 authPart := strings.Split(authHeader, " ") 38 39 if len(authPart) != 2 { 40 return false, "" 41 } 42 43 return true, authPart[1] 44 } 45 46 func getQueryToken(r *http.Request) (bool, string) { 47 token := r.URL.Query().Get("token") 48 if token == "" { 49 return false, "" 50 } 51 return true, token 52 } 53 54 func getSession(r *http.Request, tokenExtractor func(r *http.Request) (bool, string)) (Session, bool) { 55 tokenOK, token := tokenExtractor(r) 56 if !tokenOK || token == "" { 57 return Session{}, false 58 } 59 60 for i, session := range sessions { 61 if session.Token == token { 62 63 if time.Now().After(session.Expiration) { 64 // Expired. 65 sessions = append(sessions[:i], sessions[i+1:]...) 66 return Session{}, false 67 } 68 69 return session, true 70 } 71 } 72 73 return Session{}, false 74 } 75 76 func storeSession(session Session, config dogeboxd.ServerConfig) { 77 sessions = append(sessions, session) 78 79 if config.DevMode { 80 file, err := os.OpenFile(fmt.Sprintf("%s/dev-sessions.gob", config.DataDir), os.O_RDWR|os.O_CREATE, 0666) 81 if err == nil { 82 encoder := gob.NewEncoder(file) 83 err = encoder.Encode(sessions) 84 if err != nil { 85 log.Printf("Failed to encode sessions to dev-sessions.gob: %v", err) 86 } 87 file.Close() 88 } else { 89 log.Printf("Failed to open dev-sessions.gob: %v, ignoring..", err) 90 } 91 } 92 } 93 94 func newSession() (string, Session) { 95 tokenBytes := securecookie.GenerateRandomKey(32) 96 tokenHex := make([]byte, hex.EncodedLen(len(tokenBytes))) 97 hex.Encode(tokenHex, tokenBytes) 98 token := string(tokenHex) 99 session := Session{ 100 Token: token, 101 Expiration: time.Now().Add(sessionExpiry), 102 } 103 return token, session 104 } 105 106 func delSession(r *http.Request) error { 107 tokenOK, token := getBearerToken(r) 108 if !tokenOK || token == "" { 109 return errors.New("failed to fetch bearer token") 110 } 111 112 for i, session := range sessions { 113 if session.Token == token { 114 sessions = append(sessions[:i], sessions[i+1:]...) 115 return nil 116 } 117 } 118 119 return nil 120 } 121 122 func authReq(dbx dogeboxd.Dogeboxd, sm dogeboxd.StateManager, route string, next http.HandlerFunc) http.HandlerFunc { 123 if route == "POST /authenticate" { 124 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 125 next.ServeHTTP(w, r) 126 }) 127 } 128 129 tokenExtractor := getBearerToken 130 131 // Handle Websocket request authentication separately. 132 if strings.HasPrefix(route, "/ws/") { 133 tokenExtractor = getQueryToken 134 } 135 136 sessionHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 _, ok := getSession(r, tokenExtractor) 138 139 if !ok { 140 w.WriteHeader(401) 141 return 142 } 143 144 next.ServeHTTP(w, r) 145 }) 146 147 // We don't want a few routes to be locked down until the user has actually configured their system. 148 // Whitelist those here. 149 // TODO: Don't hardcode these. 150 if route == "GET /system/bootstrap" || 151 route == "POST /system/bootstrap" || 152 route == "GET /system/disks" || 153 route == "GET /system/keymaps" || 154 route == "POST /system/keymap" || 155 route == "POST /system/hostname" || 156 route == "POST /system/storage" || 157 route == "POST /system/install" || 158 route == "GET /system/network/list" || 159 route == "PUT /system/network/set-pending" || 160 route == "GET /keys" || 161 route == "POST /keys/create-master" { 162 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 163 dbxis := sm.Get().Dogebox.InitialState 164 165 if !dbxis.HasFullyConfigured { 166 // We good. 167 next.ServeHTTP(w, r) 168 return 169 } 170 171 // Still check. 172 sessionHandler.ServeHTTP(w, r) 173 }) 174 } 175 176 // Any other function should require an authed session 177 return sessionHandler 178 } 179 180 type AuthenticateRequestBody struct { 181 Password string `json:"password"` 182 } 183 184 func (t api) authenticate(w http.ResponseWriter, r *http.Request) { 185 body, err := io.ReadAll(r.Body) 186 if err != nil { 187 sendErrorResponse(w, http.StatusBadRequest, "Error reading request body") 188 return 189 } 190 defer r.Body.Close() 191 192 var requestBody AuthenticateRequestBody 193 if err := json.Unmarshal(body, &requestBody); err != nil { 194 http.Error(w, "Error parsing payload", http.StatusBadRequest) 195 return 196 } 197 198 dkmToken, dkmError, err := t.dkm.Authenticate(requestBody.Password) 199 if err != nil { 200 sendErrorResponse(w, 500, err.Error()) 201 return 202 } 203 204 if dkmError != nil { 205 sendErrorResponse(w, 403, dkmError.Error()) 206 return 207 } 208 209 if dkmToken == "" { 210 // Wrong password. 211 sendErrorResponse(w, 403, "Invalid password") 212 return 213 } 214 215 // We've authed. Save our dkm authentication token to a new session. 216 token, session := newSession() 217 session.DKM_TOKEN = dkmToken 218 storeSession(session, t.config) 219 220 sendResponse(w, map[string]any{ 221 "success": true, 222 "token": token, 223 }) 224 } 225 226 func (t api) logout(w http.ResponseWriter, r *http.Request) { 227 session, sessionOK := getSession(r, getBearerToken) 228 if !sessionOK { 229 sendErrorResponse(w, 500, "Failed to fetch session") 230 return 231 } 232 233 // Clear our DKM token first. This ensures we can still convey an error 234 // to the user if this fails for whatever reason. UI should tell them to 235 // reboot their box or something to clear all authed sessions. 236 ok, err := t.dkm.InvalidateToken(session.DKM_TOKEN) 237 if err != nil { 238 log.Println("failed to invalidate token with DKM:", err) 239 sendErrorResponse(w, 500, err.Error()) 240 return 241 } 242 243 if !ok { 244 log.Println("DKM returned ok=false when invalidating token") 245 sendErrorResponse(w, 500, "Failed to invalidate token") 246 return 247 } 248 249 delSession(r) 250 251 sendResponse(w, map[string]any{ 252 "success": true, 253 }) 254 }