/ pkg / web / session.go
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  }