Skip to content

Commit

Permalink
Added health check
Browse files Browse the repository at this point in the history
  • Loading branch information
L480 committed Oct 25, 2024
1 parent 3cb30b8 commit 2a6ddec
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 15 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ curl -H "Authorization: Bearer $API_TOKEN" \
-i https://tesla-http-api.example.com/api/1/vehicles/{VIN}/command/flash_lights
```

You can find all API endpoints in [Tesla's Fleet API documentation](https://developer.tesla.com/docs/fleet-api/endpoints/vehicle-commands).
You can find all API endpoints in [Tesla's Fleet API documentation](https://developer.tesla.com/docs/fleet-api/endpoints/vehicle-commands).

### Health check

```bash
curl -X GET \
-i https://tesla-http-api.example.com/health
```
42 changes: 29 additions & 13 deletions cmd/tesla-http-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/L480/tesla-http-api/internal/logger"
Expand All @@ -30,21 +31,36 @@ var (
apiToken string
)

func middleware(next http.Handler, teslaAccessToken string) http.Handler {
func router(next http.Handler, teslaAccessToken string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if apiTokenEnabled {
token := r.Header.Get("Authorization")
if token != apiToken {
logger.Info("Request to %s from %s \033[31m(invalid token)\033[0m", r.URL.Path, r.Header.Get("X-Forwarded-For"))
http.Error(w, http.StatusText(403), http.StatusForbidden)
path := strings.Split(r.URL.Path, "/")[1]
switch path {
case "health":
if tesla.Healthy {
http.Error(w, http.StatusText(200), http.StatusOK)
return
} else {
http.Error(w, http.StatusText(502), http.StatusBadGateway)
return
}
r.Header.Del("Authorization")
}
case "api":
if apiTokenEnabled {
token := r.Header.Get("Authorization")
if token != apiToken {
logger.Info("Request to %s from %s\033[31m(invalid token)\033[0m", r.URL.Path, r.Header.Get("X-Forwarded-For"))
http.Error(w, http.StatusText(403), http.StatusForbidden)
return
}
r.Header.Del("Authorization")
}

r.Header.Add("Authorization", "Bearer "+teslaAccessToken)
logger.Info("Request to %s from %s", r.URL.Path, r.Header.Get("X-Forwarded-For"))
next.ServeHTTP(w, r)
r.Header.Add("Authorization", "Bearer "+teslaAccessToken)
logger.Info("Request to %s from %s", r.URL.Path, r.Header.Get("X-Forwarded-For"))
next.ServeHTTP(w, r)
default:
http.Error(w, http.StatusText(404), http.StatusNotFound)
return
}
})
}

Expand All @@ -58,7 +74,7 @@ func main() {
}

if !apiTokenEnabled {
logger.Warning("\033[33m%s IS SET TO FALSE. YOUR API IS UNPROTECTED AND CAN BE USED WITHOUT AUTHENTICATION. THIS IS NOT RECOMMENDED.\033[0m", EnvApiTokenEnabled)
logger.Warning("\033[1m\033[33m%s IS SET TO FALSE. YOUR API IS UNPROTECTED AND CAN BE USED WITHOUT AUTHENTICATION. THIS IS NOT RECOMMENDED.\033[0m", EnvApiTokenEnabled)
}

go tesla.RefreshToken(config)
Expand All @@ -79,7 +95,7 @@ func main() {

accessToken, err := os.ReadFile(config.AccessTokenFile)
logger.Info("Listening on %s", addr)
logger.Error("Server stopped: %s", http.ListenAndServe(addr, middleware(p, string(accessToken))))
logger.Error("Server stopped: %s", http.ListenAndServe(addr, router(p, string(accessToken))))
}

func readFromEnvironment() error {
Expand Down
22 changes: 21 additions & 1 deletion internal/tesla/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,35 @@ type Config struct {
RefreshToken string
}

var (
Healthy bool
)

func RefreshToken(c Config) {
tokenTimer := time.NewTimer(0)
refreshInterval := 6 * time.Hour
retryInterval := 5 * time.Minute
var latestRefreshToken string

type response struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}

for {
<-tokenTimer.C
logger.Info("Refreshing access token ...")
file, err := os.ReadFile(c.RefreshTokenFile)

if err != nil {
latestRefreshToken = c.RefreshToken
} else {
latestRefreshToken = string(file)
}

form := url.Values{}
form.Add("grant_type", "refresh_token")
form.Add("client_id", c.ClientId)
Expand All @@ -52,40 +60,52 @@ func RefreshToken(c Config) {
Body: form.Encode(),
InsecureSkipVerify: false,
}

resp, err := request.Connect(tokenEndpoint)
if err != nil {
Healthy = false
logger.Error("Failed to connect to token endpoint: %s", err)
tokenTimer.Reset(retryInterval)
continue
}

body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
Healthy = false
logger.Error("Failed to retrieve response body: %s", err)
tokenTimer.Reset(retryInterval)
continue
}

if resp.StatusCode == 200 {
Healthy = true
logger.Info("Access token refresh successful")
logger.Info("Next access token refresh scheduled in %s hours", strconv.FormatFloat(refreshInterval.Hours(), 'g', 2, 64))
tokenTimer.Reset(refreshInterval)
} else {
logger.Error("Refresh failed: %s.", string(body))
Healthy = false
logger.Error("Refresh failed: %s", string(body))
logger.Info("Retrying access token refresh in %s minutes", strconv.FormatFloat(retryInterval.Minutes(), 'g', 2, 64))
tokenTimer.Reset(retryInterval)
continue
}

var jsonData response
json.Unmarshal(body, &jsonData)
accessTokenFile, err := os.Create(c.AccessTokenFile)
if err != nil {
Healthy = false
logger.Error("Failed to save access token: %s", err)
tokenTimer.Reset(retryInterval)
continue
}

accessTokenFile.WriteString(jsonData.AccessToken)
accessTokenFile.Close()
refreshTokenFile, err := os.Create(c.RefreshTokenFile)
if err != nil {
Healthy = false
logger.Error("Failed to save refresh token: %s", err)
tokenTimer.Reset(retryInterval)
continue
Expand Down

0 comments on commit 2a6ddec

Please sign in to comment.