Pair a car
{{if .Done}}
{{.Message}}
{{else}}
Check that this code matches the one on the car's screen, then approve with the approval password.
{{end}}
`))
func renderPage(w http.ResponseWriter, status int, data pageData) {
h := w.Header()
h.Set("Content-Type", "text/html; charset=utf-8")
// No scripts at all, no framing, forms only back to this page.
h.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "no-referrer")
h.Set("Cache-Control", "no-store")
w.WriteHeader(status)
if err := pairTemplate.Execute(w, data); err != nil {
slog.Warn("pair page not rendered", "error", err)
}
}
func (a *API) pairPage(w http.ResponseWriter, r *http.Request) {
if !a.Pairing.Enabled() {
http.NotFound(w, r)
return
}
data := pageData{}
if raw := r.URL.Query().Get("code"); raw != "" {
if req, err := a.Pairing.Lookup(raw); err == nil && req.state == pairingPending {
data.Code = req.userCode
} else {
data.Error = "This code is unknown or has expired. Start pairing again on the car."
}
}
renderPage(w, http.StatusOK, data)
}
func (a *API) pairDecide(w http.ResponseWriter, r *http.Request) {
if !a.Pairing.Enabled() {
http.NotFound(w, r)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 4096)
if err := r.ParseForm(); err != nil {
renderPage(w, http.StatusBadRequest, pageData{Error: "The form could not be read."})
return
}
code := normalizeUserCode(r.PostForm.Get("code"))
approve := r.PostForm.Get("action") != "deny"
device, err := a.Pairing.Decide(code, r.PostForm.Get("password"), clientAddress(r), approve)
switch {
case errors.Is(err, errWrongPassword):
renderPage(w, http.StatusUnauthorized, pageData{Code: code, Error: "Wrong password."})
return
case errors.Is(err, errTooManyFailed):
renderPage(w, http.StatusTooManyRequests, pageData{Code: code, Error: err.Error() + "."})
return
case errors.Is(err, errUnknownCode), errors.Is(err, errNotPending):
renderPage(w, http.StatusNotFound, pageData{Error: "This code is unknown, expired or already used. Start pairing again on the car."})
return
case err != nil:
renderPage(w, http.StatusInternalServerError, pageData{Code: code, Error: "Something went wrong."})
return
}
if !approve {
renderPage(w, http.StatusOK, pageData{Done: true, Message: "Denied. The car was not paired."})
return
}
// Persist before the car can collect the token: a token that vanished at the next
// restart would look like a pairing that worked.
if err := a.Store.SaveDevice(r.Context(), device); err != nil {
a.Pairing.Forget(code, device)
slog.Error("device not saved", "error", err)
renderPage(w, http.StatusInternalServerError, pageData{Code: code, Error: "The car could not be saved. Try again."})
return
}
a.Pairing.Commit(code, device)
slog.Info("car paired", "device", device.ID, "installation", device.InstallationID)
renderPage(w, http.StatusOK, pageData{Done: true, Message: "Approved. The car finishes pairing on its own within a few seconds — you can close this page."})
}
// ---------- Device management ----------
func (a *API) adminAuthorized(w http.ResponseWriter, r *http.Request) bool {
if a.pairingDisabled(w) {
return false
}
supplied, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if !ok {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return false
}
switch err := a.Pairing.CheckAdmin(supplied, clientAddress(r)); {
case errors.Is(err, errTooManyFailed):
writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": err.Error()})
return false
case err != nil:
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return false
}
return true
}
type deviceJSON struct {
ID string `json:"id"`
InstallationID string `json:"installationId"`
CreatedAt time.Time `json:"createdAt"`
Revoked bool `json:"revoked"`
}
func (a *API) listDevices(w http.ResponseWriter, r *http.Request) {
if !a.adminAuthorized(w, r) {
return
}
out := []deviceJSON{}
for _, d := range a.Pairing.Devices() {
out = append(out, deviceJSON{d.ID, d.InstallationID, d.CreatedAt, d.Revoked})
}
writeJSON(w, http.StatusOK, map[string]any{"devices": out})
}
func (a *API) revokeDevice(w http.ResponseWriter, r *http.Request) {
if !a.adminAuthorized(w, r) {
return
}
d, ok := a.Pairing.Revoke(r.PathValue("id"))
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "no such device"})
return
}
if err := a.Store.SaveDevice(r.Context(), d); err != nil {
slog.Error("revocation not saved", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "revoked until restart only; saving failed"})
return
}
slog.Info("car revoked", "device", d.ID)
w.WriteHeader(http.StatusNoContent)
}