From 92fdfc45fee9be5e9b6ef4ad6408df9cf6f6abc7 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Tue, 23 Mar 2021 11:09:00 +0100 Subject: [PATCH 01/23] bumping new version v1.3.1 updating CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 657ed12..b833a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +## [1.3.1] - 2021-03-23 + +### Changed +- fixing sql error when BatteryHeaterNoPower is null (#19 by @LelandSindt) + ## [1.3.0] - 2021-03-17 ### Added @@ -85,7 +90,8 @@ ## [1.0.0] - 2021-02-15 -[Unreleased]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.0...HEAD +[Unreleased]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.1...HEAD +[1.3.1]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.1...v1.3.1 [1.3.0]: https://github.com/tobiasehlert/teslamateapi/compare/v1.2.3...v1.3.0 [1.2.3]: https://github.com/tobiasehlert/teslamateapi/compare/v1.2.2...v1.2.3 [1.2.2]: https://github.com/tobiasehlert/teslamateapi/compare/v1.2.1...v1.2.2 From 876e06d68b87f7c0a25b25f5e82d8e55ccb8fb5a Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Tue, 9 Mar 2021 07:10:17 -0600 Subject: [PATCH 02/23] proxy commands to Tesla API --- src/v1_TeslaMateAPICarsCommand.go | 307 ++++++++++++++++++++++++++++++ src/webserver.go | 14 ++ 2 files changed, 321 insertions(+) create mode 100644 src/v1_TeslaMateAPICarsCommand.go diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go new file mode 100644 index 0000000..6ff64cc --- /dev/null +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -0,0 +1,307 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +// TeslaMateAPICarsCommandV1 func +func TeslaMateAPICarsCommandV1(c *gin.Context) { + + // creating required vars + var accessToken string + var vehicleID string + var jsonData map[string]interface{} + var err error + var command string + var requestToken string + + // verify X-Command-Token + requestToken = c.Request.Header.Get("X-Command-Token") + if requestToken != commandToken || requestToken == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) + return + } + + // if request method is GET return list of commands + if c.Request.Method == "GET" { + c.JSON(http.StatusOK, allowList) + return + } + + // getting CarID param from URL + ParamCarID := c.Param("CarID") + var CarID int + if ParamCarID != "" { + CarID = convertStringToInteger(ParamCarID) + } + + if CarID == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "CarID invalid"}) + return + } + + // getting request body to pass to Tesla + reqBody, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + // I am not a fan of hardcoding "/api/v1/" + // it would be nice to find a way to retrieve api.Group + command = (c.Request.RequestURI[len("/api/v1/cars/"+ParamCarID):]) + + if !contains(allowList, command) { + log.Print("command: " + command + " not allowed") + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) + return + } + + // allow /command/wake_up but silently `redirect` it to /wake_up + if command == "/command/wake_up" { + command = "/wake_up" + } + + // getting access token + err = db.QueryRow(` + SELECT + access + FROM tokens + LIMIT 1; + `).Scan(&accessToken) + + // checking for errors in query + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + // get vehicle ID + err = db.QueryRow(` + SELECT + eid + FROM cars + WHERE id = $1 + LIMIT 1; + `, CarID).Scan(&vehicleID) + + // ToDo: ?cleanup DB connections? -- I can't find an example of closing db.QueryRow() ¯\_(ツ)_/¯ + + // checking for errors in query + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + client := &http.Client{} + req, err := http.NewRequest("POST", "https://owner-api.teslamotors.com/api/1/vehicles/"+vehicleID+command, strings.NewReader(string(reqBody))) + req.Header.Add("Authorization", "Bearer "+accessToken) + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + + // check response error + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + defer resp.Body.Close() + defer client.CloseIdleConnections() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + json.Unmarshal([]byte(respBody), &jsonData) + + // print to log about request + if gin.IsDebugging() { + log.Println("[TeslaMateApi] TeslaMateAPICarsCommand " + command + " returned data:") + js, _ := json.Marshal(jsonData) + log.Printf("%s\n", js) + } + + c.JSON(resp.StatusCode, jsonData) +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func getCommandToken() string { + // get token from environment variable COMMAND_TOKEN + token := getEnv("COMMAND_TOKEN", "") + if token == "" || len(token) < 32 { + log.Println("Environment variable COMMAND_TOKEN not set, is empty, or too short. All commands will return unauthroized.") + token = "" + } + return token +} + +func getAllowList() []string { + + var allowAll bool = getEnvAsBool("COMMANDS_ALL", false) + + // https://tesla-api.timdorr.com/vehicle/commands/wake + if getEnvAsBool("COMMANDS_WAKE", false) || allowAll { + allowList = append(allowList, + "/command/wake_up", + "/wake_up") + } + + // https://tesla-api.timdorr.com/vehicle/commands/alerts + if getEnvAsBool("COMMANDS_ALERT", false) || allowAll { + allowList = append(allowList, + "/command/honk_horn", + "/command/flash_lights") + } + + // https://tesla-api.timdorr.com/vehicle/commands/remotestart + if getEnvAsBool("COMMANDS_REMOTESTART", false) || allowAll { + allowList = append(allowList, "/command/remote_start_drive") + } + + // https://tesla-api.timdorr.com/vehicle/commands/homelink + if getEnvAsBool("COMMANDS_HOMELINK", false) || allowAll { + allowList = append(allowList, "/command/trigger_homelink") + } + + // https://tesla-api.timdorr.com/vehicle/commands/speedlimit + if getEnvAsBool("COMMANDS_SPEEDLIMIT", false) || allowAll { + allowList = append(allowList, + "/command/speed_limit_set_limit", + "/command/speed_limit_activate", + "/command/speed_limit_deactivate", + "/command/speed_limit_clear_pin") + } + + // https://tesla-api.timdorr.com/vehicle/commands/valet + if getEnvAsBool("COMMANDS_VALET", false) || allowAll { + allowList = append(allowList, + "/command/set_valet_mode", + "/command/reset_valet_pin") + } + + // https://tesla-api.timdorr.com/vehicle/commands/sentrymode + if getEnvAsBool("COMMANDS_SENTRYMODE", false) || allowAll { + allowList = append(allowList, "/command/set_sentry_mode") + } + + // https://tesla-api.timdorr.com/vehicle/commands/doors + if getEnvAsBool("COMMANDS_DOORS", false) || allowAll { + allowList = append(allowList, + "/command/door_unlock", + "/command/door_lock") + } + + // https://tesla-api.timdorr.com/vehicle/commands/trunk + if getEnvAsBool("COMMANDS_TRUNK", false) || allowAll { + allowList = append(allowList, "/command/actuate_trunk") + } + + // https://tesla-api.timdorr.com/vehicle/commands/windows + if getEnvAsBool("COMMANDS_WINDOWS", false) || allowAll { + allowList = append(allowList, "/command/window_control") + } + + // https://tesla-api.timdorr.com/vehicle/commands/sunroof + if getEnvAsBool("COMMAND_SUNROOF", false) || allowAll { + allowList = append(allowList, "/command/sun_roof_control") + } + + // https://tesla-api.timdorr.com/vehicle/commands/charging + if getEnvAsBool("COMMANDS_CHARGING", false) || allowAll { + allowList = append(allowList, + "/command/charge_port_door_open", + "/command/charge_port_door_close", + "/command/charge_start", + "/command/charge_stop", + "/command/charge_standard", + "/command/charge_max_range", + "/command/set_charge_limit") + } + + // https://tesla-api.timdorr.com/vehicle/commands/climate + if getEnvAsBool("COMMANDS_CLIMATE", false) || allowAll { + allowList = append(allowList, + "/command/auto_conditioning_start", + "/command/auto_conditioning_stop", + "/command/set_temps", + "/command/set_preconditioning_max", + "/command/remote_seat_heater_request", + "/command/remote_steering_wheel_heater_request") + } + + // https://tesla-api.timdorr.com/vehicle/commands/media + if getEnvAsBool("COMMANDS_MEDIA", false) || allowAll { + allowList = append(allowList, + "/command/media_toggle_playback", + "/command/media_next_track", + "/command/media_prev_track", + "/command/media_next_fav", + "/command/media_prev_fav", + "/command/media_volume_up", + "/command/media_volume_down") + } + + // https://tesla-api.timdorr.com/vehicle/commands/sharing + if getEnvAsBool("COMMANDS_SHARING", false) || allowAll { + allowList = append(allowList, "/command/share") + } + + // https://tesla-api.timdorr.com/vehicle/commands/softwareupdate + if getEnvAsBool("COMMANDS_SOFTWAREUPDATE", false) || allowAll { + allowList = append(allowList, + "/command/schedule_software_update", + "/command/cancel_software_update") + } + + // if allowList is empty, read COMMANDS_ALLOWLIST and append to allowList + commandAllowListLocation := getEnv("COMMANDS_ALLOWLIST", "allow_list.json") + if len(allowList) == 0 { + var allowListFile []string + commandAllowListFile, err := os.Open(commandAllowListLocation) + if err != nil { + log.Println("COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") + } else { + byteValue, err := ioutil.ReadAll(commandAllowListFile) + if err != nil { + log.Println("error reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + } else { + err = json.Unmarshal(byteValue, &allowListFile) + if err != nil { + log.Println("error parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + } else { + allowList = append(allowList, allowListFile...) + commandAllowListFile.Close() + } + } + } + } else { + log.Print("COMMANDS from environemnt variables set, " + commandAllowListLocation + " will be ignored.") + } + + log.Println("List of allowed Commands: " + strings.Join(allowList, ", ")) + + return allowList + +} diff --git a/src/webserver.go b/src/webserver.go index ba09fa9..28acd8b 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -17,6 +17,12 @@ import ( // defining db var var db *sql.DB +// list of allowed commands +var allowList []string + +// token to authroize commands +var commandToken string + // main function func main() { @@ -38,6 +44,9 @@ func main() { initDBconnection() defer db.Close() + commandToken = getCommandToken() + allowList = getAllowList() + // kicking off Gin in value r r := gin.Default() @@ -71,6 +80,11 @@ func main() { v1.GET("/cars/:CarID/status", TeslaMateAPICarsStatusV1) v1.GET("/cars/:CarID/updates", TeslaMateAPICarsUpdatesV1) v1.GET("/globalsettings", TeslaMateAPIGlobalsettingsV1) + + v1.GET("/cars/:CarID/command", TeslaMateAPICarsCommandV1) + + v1.POST("/cars/:CarID/command/:Command", TeslaMateAPICarsCommandV1) + v1.POST("/cars/:CarID/wake_up", TeslaMateAPICarsCommandV1) } // /api/ping endpoint From c88e0fa6f8a4a904454e9c3e4f380d973a231093 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Tue, 9 Mar 2021 22:02:48 +0100 Subject: [PATCH 03/23] fixing typo and some small things adding README info of commands with documentation --- README.md | 26 ++++++++++++++++++++++++++ src/v1_TeslaMateAPICarsCommand.go | 6 ++++-- src/webserver.go | 8 ++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 42a5dbb..ce6db4b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ TeslaMateApi is a RESTful API to get data collected by self-hosted data logger * - Written in **[Golang](https://golang.org/)** - Data is collected from TeslaMate **Postgres** database and local **MQTT** Broker - Endpoints return data in JSON format +- Send commands to your Tesla through the TeslaMateApi ### Table of Contents @@ -108,6 +109,28 @@ Basically the same environment variables for the database, mqqt and timezone nee - **MQTT_NAMESPACE** string *(default: )* - **MQTT_SLEEPTIME** integer *(default: 100)* +**Commands** environment variables + +- **ENABLE_COMMANDS** boolean *(default: false)* +- **COMMANDS_ALL** boolean *(default: false)* +- **COMMANDS_ALLOWLIST** string *(default: allow_list.json)* +- **COMMANDS_WAKE** boolean *(default: false)* +- **COMMANDS_ALERT** boolean *(default: false)* +- **COMMANDS_REMOTESTART** boolean *(default: false)* +- **COMMANDS_HOMELINK** boolean *(default: false)* +- **COMMANDS_SPEEDLIMIT** boolean *(default: false)* +- **COMMANDS_VALET** boolean *(default: false)* +- **COMMANDS_SENTRYMODE** boolean *(default: false)* +- **COMMANDS_DOORS** boolean *(default: false)* +- **COMMANDS_TRUNK** boolean *(default: false)* +- **COMMANDS_WINDOWS** boolean *(default: false)* +- **COMMANDS_SUNROOF** boolean *(default: false)* +- **COMMANDS_CHARGING** boolean *(default: false)* +- **COMMANDS_CLIMATE** boolean *(default: false)* +- **COMMANDS_MEDIA** boolean *(default: false)* +- **COMMANDS_SHARING** boolean *(default: false)* +- **COMMANDS_SOFTWAREUPDATE** boolean *(default: false)* + ## API documentation More detailed documentation of every endpoint will come.. @@ -118,10 +141,13 @@ More detailed documentation of every endpoint will come.. - GET `/api/v1/cars/:CarID` - GET `/api/v1/cars/:CarID/charges` - GET `/api/v1/cars/:CarID/charges/:ChargeID` +- GET `/api/v1/cars/:CarID/command` +- POST `/api/v1/cars/:CarID/command/:Command` - GET `/api/v1/cars/:CarID/drives` - GET `/api/v1/cars/:CarID/drives/:DriveID` - GET `/api/v1/cars/:CarID/status` - GET `/api/v1/cars/:CarID/updates` +- POST `/api/v1/cars/:CarID/wake_up` - GET `/api/v1/globalsettings` - GET `/api/ping` diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 6ff64cc..5cd0103 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -43,7 +43,9 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { CarID = convertStringToInteger(ParamCarID) } + // validating that CarID is not zero if CarID == 0 { + log.Println("[error] TeslaMateAPICarsCommand CarID is invalid (zero)!") c.JSON(http.StatusBadRequest, gin.H{"error": "CarID invalid"}) return } @@ -159,7 +161,7 @@ func getCommandToken() string { func getAllowList() []string { - var allowAll bool = getEnvAsBool("COMMANDS_ALL", false) + allowAll := getEnvAsBool("COMMANDS_ALL", false) // https://tesla-api.timdorr.com/vehicle/commands/wake if getEnvAsBool("COMMANDS_WAKE", false) || allowAll { @@ -224,7 +226,7 @@ func getAllowList() []string { } // https://tesla-api.timdorr.com/vehicle/commands/sunroof - if getEnvAsBool("COMMAND_SUNROOF", false) || allowAll { + if getEnvAsBool("COMMANDS_SUNROOF", false) || allowAll { allowList = append(allowList, "/command/sun_roof_control") } diff --git a/src/webserver.go b/src/webserver.go index 28acd8b..5ee5b20 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -75,16 +75,16 @@ func main() { v1.GET("/cars/:CarID", TeslaMateAPICarsV1) v1.GET("/cars/:CarID/charges", TeslaMateAPICarsChargesV1) v1.GET("/cars/:CarID/charges/:ChargeID", TeslaMateAPICarsChargesDetailsV1) + v1.GET("/cars/:CarID/commands", TeslaMateAPICarsCommandV1) + v1.POST("/cars/:CarID/commands/:Command", TeslaMateAPICarsCommandV1) v1.GET("/cars/:CarID/drives", TeslaMateAPICarsDrivesV1) v1.GET("/cars/:CarID/drives/:DriveID", TeslaMateAPICarsDrivesDetailsV1) v1.GET("/cars/:CarID/status", TeslaMateAPICarsStatusV1) + v1.GET("/cars/:CarID/updates", TeslaMateAPICarsUpdatesV1) - v1.GET("/globalsettings", TeslaMateAPIGlobalsettingsV1) - v1.GET("/cars/:CarID/command", TeslaMateAPICarsCommandV1) - - v1.POST("/cars/:CarID/command/:Command", TeslaMateAPICarsCommandV1) v1.POST("/cars/:CarID/wake_up", TeslaMateAPICarsCommandV1) + v1.GET("/globalsettings", TeslaMateAPIGlobalsettingsV1) } // /api/ping endpoint From 77a63a5ac701b9594e47b40290b8ec480b241aef Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Tue, 9 Mar 2021 22:04:16 +0100 Subject: [PATCH 04/23] adding comments enhancement of logging --- src/v1_TeslaMateAPICarsCommand.go | 29 +++++++++++++++-------------- src/webserver.go | 13 +++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 5cd0103..54e79bd 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -26,6 +26,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // verify X-Command-Token requestToken = c.Request.Header.Get("X-Command-Token") if requestToken != commandToken || requestToken == "" { + log.Println("[error] TeslaMateAPICarsCommand missing X-Command-Token header.. throwing error!") c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) return } @@ -53,7 +54,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // getting request body to pass to Tesla reqBody, err := ioutil.ReadAll(c.Request.Body) if err != nil { - log.Println(err) + log.Println("[error] TeslaMateAPICarsCommand error in first ioutil.ReadAll", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -63,7 +64,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { command = (c.Request.RequestURI[len("/api/v1/cars/"+ParamCarID):]) if !contains(allowList, command) { - log.Print("command: " + command + " not allowed") + log.Print("[warning] TeslaMateAPICarsCommand command: " + command + " not allowed") c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) return } @@ -83,7 +84,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // checking for errors in query if err != nil { - log.Println(err) + log.Println("[error] TeslaMateAPICarsCommand error in token sql query ", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -101,7 +102,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // checking for errors in query if err != nil { - log.Println(err) + log.Println("[error] TeslaMateAPICarsCommand error in cars sql query ", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -114,7 +115,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // check response error if err != nil { - log.Println(err) + log.Println("[error] TeslaMateAPICarsCommand error in http request to owner-api.teslamotors.com ", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -124,7 +125,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { respBody, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Println(err) + log.Println("[error] TeslaMateAPICarsCommand error in second ioutil.ReadAll ", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -132,9 +133,9 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // print to log about request if gin.IsDebugging() { - log.Println("[TeslaMateApi] TeslaMateAPICarsCommand " + command + " returned data:") + log.Println("[debug] TeslaMateAPICarsCommand " + command + " returned data:") js, _ := json.Marshal(jsonData) - log.Printf("%s\n", js) + log.Printf("[debug] %s\n", js) } c.JSON(resp.StatusCode, jsonData) @@ -153,7 +154,7 @@ func getCommandToken() string { // get token from environment variable COMMAND_TOKEN token := getEnv("COMMAND_TOKEN", "") if token == "" || len(token) < 32 { - log.Println("Environment variable COMMAND_TOKEN not set, is empty, or too short. All commands will return unauthroized.") + log.Println("[warning] getCommandToken environment variable COMMAND_TOKEN not set, is empty, or too short. All POST commands will return unauthroized.") token = "" } return token @@ -283,15 +284,15 @@ func getAllowList() []string { var allowListFile []string commandAllowListFile, err := os.Open(commandAllowListLocation) if err != nil { - log.Println("COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") + log.Println("[error] getAllowList error with COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") } else { byteValue, err := ioutil.ReadAll(commandAllowListFile) if err != nil { - log.Println("error reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + log.Println("[error] getAllowList error while reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") } else { err = json.Unmarshal(byteValue, &allowListFile) if err != nil { - log.Println("error parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + log.Println("[error] getAllowList error while parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") } else { allowList = append(allowList, allowListFile...) commandAllowListFile.Close() @@ -299,10 +300,10 @@ func getAllowList() []string { } } } else { - log.Print("COMMANDS from environemnt variables set, " + commandAllowListLocation + " will be ignored.") + log.Print("[info] getAllowList COMMANDS from environment variables set, " + commandAllowListLocation + " will be ignored.") } - log.Println("List of allowed Commands: " + strings.Join(allowList, ", ")) + log.Println("[info] getAllowList list of allowed Commands: " + strings.Join(allowList, ", ")) return allowList diff --git a/src/webserver.go b/src/webserver.go index 5ee5b20..d626cdc 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -71,19 +71,32 @@ func main() { c.JSON(http.StatusOK, gin.H{"message": "TeslaMateApi v1 runnnig..", "path": "/api/v1"}) }) + // v1 /api/v1/cars endpoints v1.GET("/cars", TeslaMateAPICarsV1) v1.GET("/cars/:CarID", TeslaMateAPICarsV1) + + // v1 /api/v1/cars/:CarID/charges endpoints v1.GET("/cars/:CarID/charges", TeslaMateAPICarsChargesV1) v1.GET("/cars/:CarID/charges/:ChargeID", TeslaMateAPICarsChargesDetailsV1) + + // v1 /api/v1/cars/:CarID/command endpoints v1.GET("/cars/:CarID/commands", TeslaMateAPICarsCommandV1) v1.POST("/cars/:CarID/commands/:Command", TeslaMateAPICarsCommandV1) + + // v1 /api/v1/cars/:CarID/drives endpoints v1.GET("/cars/:CarID/drives", TeslaMateAPICarsDrivesV1) v1.GET("/cars/:CarID/drives/:DriveID", TeslaMateAPICarsDrivesDetailsV1) + + // v1 /api/v1/cars/:CarID/status endpoints v1.GET("/cars/:CarID/status", TeslaMateAPICarsStatusV1) + // v1 /api/v1/cars/:CarID/updates endpoints v1.GET("/cars/:CarID/updates", TeslaMateAPICarsUpdatesV1) + // v1 /api/v1/cars/:CarID/wake_up endpoints v1.POST("/cars/:CarID/wake_up", TeslaMateAPICarsCommandV1) + + // v1 /api/v1/globalsettings endpoints v1.GET("/globalsettings", TeslaMateAPIGlobalsettingsV1) } From 97ae4f6cbb08479c52ebeea4e2cf0f35a3039da6 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Thu, 11 Mar 2021 22:59:42 +0100 Subject: [PATCH 05/23] changing from commands path to command moving function checkArrayContainsString --- src/v1_TeslaMateAPICarsCommand.go | 11 +---------- src/webserver.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 54e79bd..0a8eec5 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -63,8 +63,8 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // it would be nice to find a way to retrieve api.Group command = (c.Request.RequestURI[len("/api/v1/cars/"+ParamCarID):]) - if !contains(allowList, command) { log.Print("[warning] TeslaMateAPICarsCommand command: " + command + " not allowed") + if !checkArrayContainsString(allowList, command) { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) return } @@ -141,15 +141,6 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { c.JSON(resp.StatusCode, jsonData) } -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - func getCommandToken() string { // get token from environment variable COMMAND_TOKEN token := getEnv("COMMAND_TOKEN", "") diff --git a/src/webserver.go b/src/webserver.go index d626cdc..a92f9cd 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -80,8 +80,8 @@ func main() { v1.GET("/cars/:CarID/charges/:ChargeID", TeslaMateAPICarsChargesDetailsV1) // v1 /api/v1/cars/:CarID/command endpoints - v1.GET("/cars/:CarID/commands", TeslaMateAPICarsCommandV1) - v1.POST("/cars/:CarID/commands/:Command", TeslaMateAPICarsCommandV1) + v1.GET("/cars/:CarID/command", TeslaMateAPICarsCommandV1) + v1.POST("/cars/:CarID/command/:Command", TeslaMateAPICarsCommandV1) // v1 /api/v1/cars/:CarID/drives endpoints v1.GET("/cars/:CarID/drives", TeslaMateAPICarsDrivesV1) @@ -303,3 +303,13 @@ func fahrenheitToCelsiusNilSupport(f NullFloat64) NullFloat64 { f.Float64 = ((f.Float64 - 32) * 5 / 9) return (f) } + +// checkArrayContainsString func - check if string is inside stringarray +func checkArrayContainsString(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} From 9f7265e210e145e7d322d9df15a01d6cc91679cd Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Thu, 11 Mar 2021 23:02:09 +0100 Subject: [PATCH 06/23] adding AuthSupport file related changes in files --- README.md | 1 + src/AuthSupport.go | 97 +++++++++++++++++++++++++++++++ src/v1_TeslaMateAPICarsCommand.go | 7 +++ src/webserver.go | 9 +-- 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 src/AuthSupport.go diff --git a/README.md b/README.md index ce6db4b..ede0433 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Basically the same environment variables for the database, mqqt and timezone nee **Optional** environment variables +- **API_TOKEN** string *(default: )* - **DATABASE_PORT** integer *(default: 5432)* - **DATABASE_TIMEOUT** integer *(default: 60000)* - **DATABASE_SSL** boolean *(default: true)* diff --git a/src/AuthSupport.go b/src/AuthSupport.go new file mode 100644 index 0000000..de81371 --- /dev/null +++ b/src/AuthSupport.go @@ -0,0 +1,97 @@ +package main + +import ( + "log" + + "github.com/gin-gonic/gin" +) + +// initAuthToken func +func initAuthToken() { + // get token from environment variable API_TOKEN + envToken = getEnv("API_TOKEN", "") + if envToken == "" { + log.Println("[warning] initAuthToken - environment variable API_TOKEN not set or is empty.") + } else if len(envToken) < 32 { + log.Println("[warning] initAuthToken - environment variable API_TOKEN too short.. should be 32 or longer.") + } else { + log.Println("[info] initAuthToken - environment variable API_TOKEN is set and good.") + } +} + +// validateAuthToken func +func validateAuthToken(c *gin.Context) (bool, string) { + + // trying with http parameter - ?token= + tokenParamsValue := c.DefaultQuery("token", "") + + // if validTokenParams is longer than zero + if len(tokenParamsValue) > 0 { + + // checking if token is valid (since it's over zero length) + if checkAuthToken(tokenParamsValue) { + // the token is valid! + log.Println("[debug] validateAuthToken - param token valid.") + return true, "" + + } + // the token is invalid. + log.Println("[info] validateAuthToken - param token invalid.. returning 401") + return false, "param token invalid" + } + + /* + // NOT fully ready I think.. + + // trying with http header - Authorization: Bearer + reqHeaderToken := c.Request.Header.Get("Authorization") + + // if length of reqHeaderToken is more than zero + if len(reqHeaderToken) > 0 { + + // removing Bearer part from header to get token out of header + splitToken := strings.Split(reqHeaderToken, "Bearer") + + if len(splitToken) != 2 { + // bearer token is not proper formatted.. returning bad request + log.Println("[info] validateAuthToken - header authorization bearer token is not proper formatted.. returning 401") + return false, "header authorization bearer token is not proper formatted" + + } else if strings.TrimSpace(splitToken[1]) == "" { + // bearer token is empty string.. we'll return unauthozied + log.Println("[info] validateAuthToken - header authorization bearer token is empty.. returning 401") + return false, "header authorization bearer token is empty" + + } else if checkAuthToken(strings.TrimSpace(splitToken[1])) { + // the bearer token is valid! + log.Println("[debug] validateAuthToken - header authorization bearer token valid.") + return true, "" + + } + // the check did fail.. bearer token is invalid + log.Println("[info] validateAuthToken - header authorization bearer token invalid.. returning 401") + return false, "header authorization bearer token invalid" + } + */ + + // unauthozie all calls! + return false, "failed validation" +} + +// checkAuthToken func +func checkAuthToken(token string) bool { + // checking if it's valid or not + if token == envToken { + // check that envToken was longer than zero + if len(envToken) == 0 { + log.Println("[warning] checkAuthToken - returning false (API_TOKEN is not set or empty)") + return false + } + log.Println("[info] checkAuthToken - returning true") + return true + } + + // failing check what so ever.. + log.Println("[info] checkAuthToken - returning false (other reason)") + return false +} diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 0a8eec5..00d2893 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -37,6 +37,13 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { return } + // authentication for the endpoint + validToken, errorMessage := validateAuthToken(c) + if !validToken { + c.JSON(http.StatusUnauthorized, gin.H{"error": errorMessage}) + return + } + // getting CarID param from URL ParamCarID := c.Param("CarID") var CarID int diff --git a/src/webserver.go b/src/webserver.go index a92f9cd..039109f 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -17,12 +17,12 @@ import ( // defining db var var db *sql.DB +// defining envToken that contains API_TOKEN value +var envToken string + // list of allowed commands var allowList []string -// token to authroize commands -var commandToken string - // main function func main() { @@ -44,8 +44,9 @@ func main() { initDBconnection() defer db.Close() - commandToken = getCommandToken() allowList = getAllowList() + // run initAuthToken to validate environment vars + initAuthToken() // kicking off Gin in value r r := gin.Default() From 6340c45d144e82e3e093217aa93d1866927d5269 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Thu, 11 Mar 2021 23:04:44 +0100 Subject: [PATCH 07/23] adding CommandSupport file related changes in files --- src/CommandSupport.go | 160 ++++++++++++++++++++++++++++++ src/v1_TeslaMateAPICarsCommand.go | 144 --------------------------- src/webserver.go | 3 +- 3 files changed, 162 insertions(+), 145 deletions(-) create mode 100644 src/CommandSupport.go diff --git a/src/CommandSupport.go b/src/CommandSupport.go new file mode 100644 index 0000000..c18a812 --- /dev/null +++ b/src/CommandSupport.go @@ -0,0 +1,160 @@ +package main + +import ( + "log" + "strings" + + "github.com/gin-gonic/gin" +) + +// initCommandAllowList func +func initCommandAllowList() { + + // allow all commands available below + allowAll := getEnvAsBool("COMMANDS_ALL", false) + + // https://tesla-api.timdorr.com/vehicle/commands/wake + if getEnvAsBool("COMMANDS_WAKE", false) || allowAll { + allowList = append(allowList, "/wake_up") + } + + // https://tesla-api.timdorr.com/vehicle/commands/alerts + if getEnvAsBool("COMMANDS_ALERT", false) || allowAll { + allowList = append(allowList, + "/command/honk_horn", + "/command/flash_lights") + } + + // https://tesla-api.timdorr.com/vehicle/commands/remotestart + if getEnvAsBool("COMMANDS_REMOTESTART", false) || allowAll { + allowList = append(allowList, "/command/remote_start_drive") + } + + // https://tesla-api.timdorr.com/vehicle/commands/homelink + if getEnvAsBool("COMMANDS_HOMELINK", false) || allowAll { + allowList = append(allowList, "/command/trigger_homelink") + } + + // https://tesla-api.timdorr.com/vehicle/commands/speedlimit + if getEnvAsBool("COMMANDS_SPEEDLIMIT", false) || allowAll { + allowList = append(allowList, + "/command/speed_limit_set_limit", + "/command/speed_limit_activate", + "/command/speed_limit_deactivate", + "/command/speed_limit_clear_pin") + } + + // https://tesla-api.timdorr.com/vehicle/commands/valet + if getEnvAsBool("COMMANDS_VALET", false) || allowAll { + allowList = append(allowList, + "/command/set_valet_mode", + "/command/reset_valet_pin") + } + + // https://tesla-api.timdorr.com/vehicle/commands/sentrymode + if getEnvAsBool("COMMANDS_SENTRYMODE", false) || allowAll { + allowList = append(allowList, "/command/set_sentry_mode") + } + + // https://tesla-api.timdorr.com/vehicle/commands/doors + if getEnvAsBool("COMMANDS_DOORS", false) || allowAll { + allowList = append(allowList, + "/command/door_unlock", + "/command/door_lock") + } + + // https://tesla-api.timdorr.com/vehicle/commands/trunk + if getEnvAsBool("COMMANDS_TRUNK", false) || allowAll { + allowList = append(allowList, "/command/actuate_trunk") + } + + // https://tesla-api.timdorr.com/vehicle/commands/windows + if getEnvAsBool("COMMANDS_WINDOWS", false) || allowAll { + allowList = append(allowList, "/command/window_control") + } + + // https://tesla-api.timdorr.com/vehicle/commands/sunroof + if getEnvAsBool("COMMANDS_SUNROOF", false) || allowAll { + allowList = append(allowList, "/command/sun_roof_control") + } + + // https://tesla-api.timdorr.com/vehicle/commands/charging + if getEnvAsBool("COMMANDS_CHARGING", false) || allowAll { + allowList = append(allowList, + "/command/charge_port_door_open", + "/command/charge_port_door_close", + "/command/charge_start", + "/command/charge_stop", + "/command/charge_standard", + "/command/charge_max_range", + "/command/set_charge_limit") + } + + // https://tesla-api.timdorr.com/vehicle/commands/climate + if getEnvAsBool("COMMANDS_CLIMATE", false) || allowAll { + allowList = append(allowList, + "/command/auto_conditioning_start", + "/command/auto_conditioning_stop", + "/command/set_temps", + "/command/set_preconditioning_max", + "/command/remote_seat_heater_request", + "/command/remote_steering_wheel_heater_request") + } + + // https://tesla-api.timdorr.com/vehicle/commands/media + if getEnvAsBool("COMMANDS_MEDIA", false) || allowAll { + allowList = append(allowList, + "/command/media_toggle_playback", + "/command/media_next_track", + "/command/media_prev_track", + "/command/media_next_fav", + "/command/media_prev_fav", + "/command/media_volume_up", + "/command/media_volume_down") + } + + // https://tesla-api.timdorr.com/vehicle/commands/sharing + if getEnvAsBool("COMMANDS_SHARING", false) || allowAll { + allowList = append(allowList, "/command/share") + } + + // https://tesla-api.timdorr.com/vehicle/commands/softwareupdate + if getEnvAsBool("COMMANDS_SOFTWAREUPDATE", false) || allowAll { + allowList = append(allowList, + "/command/schedule_software_update", + "/command/cancel_software_update") + } + + /* + // TODO: add back COMMANDS_ALLOWLIST check.. + + // if allowList is empty, read COMMANDS_ALLOWLIST and append to allowList + commandAllowListLocation := getEnv("COMMANDS_ALLOWLIST", "allow_list.json") + if len(allowList) == 0 { + var allowListFile []string + commandAllowListFile, err := os.Open(commandAllowListLocation) + if err != nil { + log.Println("[error] getAllowList error with COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") + } else { + byteValue, err := ioutil.ReadAll(commandAllowListFile) + if err != nil { + log.Println("[error] getAllowList error while reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + } else { + err = json.Unmarshal(byteValue, &allowListFile) + if err != nil { + log.Println("[error] getAllowList error while parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + } else { + allowList = append(allowList, allowListFile...) + commandAllowListFile.Close() + } + } + } + } else { + log.Print("[info] getAllowList COMMANDS from environment variables set, " + commandAllowListLocation + " will be ignored.") + } + */ + + if gin.IsDebugging() { + log.Println("[info] initCommandAllowList - generated following list of allowed commands: " + strings.Join(allowList, ", ")) + } +} diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 00d2893..6046167 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -158,151 +158,7 @@ func getCommandToken() string { return token } -func getAllowList() []string { - - allowAll := getEnvAsBool("COMMANDS_ALL", false) - - // https://tesla-api.timdorr.com/vehicle/commands/wake - if getEnvAsBool("COMMANDS_WAKE", false) || allowAll { - allowList = append(allowList, - "/command/wake_up", - "/wake_up") - } - - // https://tesla-api.timdorr.com/vehicle/commands/alerts - if getEnvAsBool("COMMANDS_ALERT", false) || allowAll { - allowList = append(allowList, - "/command/honk_horn", - "/command/flash_lights") - } - - // https://tesla-api.timdorr.com/vehicle/commands/remotestart - if getEnvAsBool("COMMANDS_REMOTESTART", false) || allowAll { - allowList = append(allowList, "/command/remote_start_drive") - } - - // https://tesla-api.timdorr.com/vehicle/commands/homelink - if getEnvAsBool("COMMANDS_HOMELINK", false) || allowAll { - allowList = append(allowList, "/command/trigger_homelink") - } - - // https://tesla-api.timdorr.com/vehicle/commands/speedlimit - if getEnvAsBool("COMMANDS_SPEEDLIMIT", false) || allowAll { - allowList = append(allowList, - "/command/speed_limit_set_limit", - "/command/speed_limit_activate", - "/command/speed_limit_deactivate", - "/command/speed_limit_clear_pin") - } - - // https://tesla-api.timdorr.com/vehicle/commands/valet - if getEnvAsBool("COMMANDS_VALET", false) || allowAll { - allowList = append(allowList, - "/command/set_valet_mode", - "/command/reset_valet_pin") - } - - // https://tesla-api.timdorr.com/vehicle/commands/sentrymode - if getEnvAsBool("COMMANDS_SENTRYMODE", false) || allowAll { - allowList = append(allowList, "/command/set_sentry_mode") - } - - // https://tesla-api.timdorr.com/vehicle/commands/doors - if getEnvAsBool("COMMANDS_DOORS", false) || allowAll { - allowList = append(allowList, - "/command/door_unlock", - "/command/door_lock") - } - - // https://tesla-api.timdorr.com/vehicle/commands/trunk - if getEnvAsBool("COMMANDS_TRUNK", false) || allowAll { - allowList = append(allowList, "/command/actuate_trunk") - } - - // https://tesla-api.timdorr.com/vehicle/commands/windows - if getEnvAsBool("COMMANDS_WINDOWS", false) || allowAll { - allowList = append(allowList, "/command/window_control") - } - - // https://tesla-api.timdorr.com/vehicle/commands/sunroof - if getEnvAsBool("COMMANDS_SUNROOF", false) || allowAll { - allowList = append(allowList, "/command/sun_roof_control") - } - - // https://tesla-api.timdorr.com/vehicle/commands/charging - if getEnvAsBool("COMMANDS_CHARGING", false) || allowAll { - allowList = append(allowList, - "/command/charge_port_door_open", - "/command/charge_port_door_close", - "/command/charge_start", - "/command/charge_stop", - "/command/charge_standard", - "/command/charge_max_range", - "/command/set_charge_limit") - } - - // https://tesla-api.timdorr.com/vehicle/commands/climate - if getEnvAsBool("COMMANDS_CLIMATE", false) || allowAll { - allowList = append(allowList, - "/command/auto_conditioning_start", - "/command/auto_conditioning_stop", - "/command/set_temps", - "/command/set_preconditioning_max", - "/command/remote_seat_heater_request", - "/command/remote_steering_wheel_heater_request") - } - - // https://tesla-api.timdorr.com/vehicle/commands/media - if getEnvAsBool("COMMANDS_MEDIA", false) || allowAll { - allowList = append(allowList, - "/command/media_toggle_playback", - "/command/media_next_track", - "/command/media_prev_track", - "/command/media_next_fav", - "/command/media_prev_fav", - "/command/media_volume_up", - "/command/media_volume_down") - } - - // https://tesla-api.timdorr.com/vehicle/commands/sharing - if getEnvAsBool("COMMANDS_SHARING", false) || allowAll { - allowList = append(allowList, "/command/share") - } - - // https://tesla-api.timdorr.com/vehicle/commands/softwareupdate - if getEnvAsBool("COMMANDS_SOFTWAREUPDATE", false) || allowAll { - allowList = append(allowList, - "/command/schedule_software_update", - "/command/cancel_software_update") - } - - // if allowList is empty, read COMMANDS_ALLOWLIST and append to allowList - commandAllowListLocation := getEnv("COMMANDS_ALLOWLIST", "allow_list.json") - if len(allowList) == 0 { - var allowListFile []string - commandAllowListFile, err := os.Open(commandAllowListLocation) - if err != nil { - log.Println("[error] getAllowList error with COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") - } else { - byteValue, err := ioutil.ReadAll(commandAllowListFile) - if err != nil { - log.Println("[error] getAllowList error while reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") - } else { - err = json.Unmarshal(byteValue, &allowListFile) - if err != nil { - log.Println("[error] getAllowList error while parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") - } else { - allowList = append(allowList, allowListFile...) - commandAllowListFile.Close() - } - } - } } else { log.Print("[info] getAllowList COMMANDS from environment variables set, " + commandAllowListLocation + " will be ignored.") } - - log.Println("[info] getAllowList list of allowed Commands: " + strings.Join(allowList, ", ")) - - return allowList - } diff --git a/src/webserver.go b/src/webserver.go index 039109f..c8be1c2 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -44,9 +44,10 @@ func main() { initDBconnection() defer db.Close() - allowList = getAllowList() // run initAuthToken to validate environment vars initAuthToken() + // initialize allowList stored for /command section + initCommandAllowList() // kicking off Gin in value r r := gin.Default() From b6f502ddd6325bb1748b8d64150e8e13233d5b74 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Thu, 11 Mar 2021 23:06:50 +0100 Subject: [PATCH 08/23] merging sql queries enhancing logging --- src/v1_TeslaMateAPICarsCommand.go | 102 ++++++++++++------------------ 1 file changed, 42 insertions(+), 60 deletions(-) diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 6046167..92fbb73 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "log" "net/http" - "os" "strings" "github.com/gin-gonic/gin" @@ -16,24 +15,21 @@ import ( func TeslaMateAPICarsCommandV1(c *gin.Context) { // creating required vars - var accessToken string - var vehicleID string + var TeslaAccessToken, TeslaVehicleID string var jsonData map[string]interface{} var err error var command string - var requestToken string - // verify X-Command-Token - requestToken = c.Request.Header.Get("X-Command-Token") - if requestToken != commandToken || requestToken == "" { - log.Println("[error] TeslaMateAPICarsCommand missing X-Command-Token header.. throwing error!") - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) + // check if commands are enabled.. if not we need to abort + if getEnvAsBool("ENABLE_COMMANDS", false) == false { + log.Println("[warning] TeslaMateAPICarsCommandV1 ENABLE_COMMANDS is not true.. returning 403 forbidden.") + c.JSON(http.StatusForbidden, gin.H{"error": "You are not allowed to access commands"}) return } // if request method is GET return list of commands if c.Request.Method == "GET" { - c.JSON(http.StatusOK, allowList) + c.JSON(http.StatusOK, gin.H{"enabled_commands": allowList}) return } @@ -53,7 +49,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // validating that CarID is not zero if CarID == 0 { - log.Println("[error] TeslaMateAPICarsCommand CarID is invalid (zero)!") + log.Println("[error] TeslaMateAPICarsCommandV1 CarID is invalid (zero)!") c.JSON(http.StatusBadRequest, gin.H{"error": "CarID invalid"}) return } @@ -61,7 +57,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // getting request body to pass to Tesla reqBody, err := ioutil.ReadAll(c.Request.Body) if err != nil { - log.Println("[error] TeslaMateAPICarsCommand error in first ioutil.ReadAll", err) + log.Println("[error] TeslaMateAPICarsCommandV1 error in first ioutil.ReadAll", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -76,53 +72,49 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { return } - // allow /command/wake_up but silently `redirect` it to /wake_up - if command == "/command/wake_up" { - command = "/wake_up" - } - - // getting access token - err = db.QueryRow(` + // get TeslaVehicleID and TeslaAccessToken + query := ` SELECT - access - FROM tokens - LIMIT 1; - `).Scan(&accessToken) - - // checking for errors in query - if err != nil { - log.Println("[error] TeslaMateAPICarsCommand error in token sql query ", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) - return - } - - // get vehicle ID - err = db.QueryRow(` - SELECT - eid + eid as TeslaVehicleID, + (SELECT access FROM tokens LIMIT 1) as TeslaAccessToken FROM cars WHERE id = $1 - LIMIT 1; - `, CarID).Scan(&vehicleID) - - // ToDo: ?cleanup DB connections? -- I can't find an example of closing db.QueryRow() ¯\_(ツ)_/¯ + LIMIT 1;` + rows, err := db.Query(query, CarID) // checking for errors in query if err != nil { - log.Println("[error] TeslaMateAPICarsCommand error in cars sql query ", err) + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results (even if it's only one..) + for rows.Next() { + // scanning row and putting values into the drive + err = rows.Scan( + &TeslaVehicleID, + &TeslaAccessToken, + ) + } + + // checking for errors in query when doing scan action + if err != nil { + log.Println("[error] TeslaMateAPICarsCommandV1 error in sql query:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } client := &http.Client{} - req, err := http.NewRequest("POST", "https://owner-api.teslamotors.com/api/1/vehicles/"+vehicleID+command, strings.NewReader(string(reqBody))) - req.Header.Add("Authorization", "Bearer "+accessToken) + req, err := http.NewRequest("POST", "https://owner-api.teslamotors.com/api/1/vehicles/"+TeslaVehicleID+command, strings.NewReader(string(reqBody))) + req.Header.Add("Authorization", "Bearer "+TeslaAccessToken) req.Header.Add("Content-Type", "application/json") resp, err := client.Do(req) // check response error if err != nil { - log.Println("[error] TeslaMateAPICarsCommand error in http request to owner-api.teslamotors.com ", err) + log.Println("[error] TeslaMateAPICarsCommandV1 error in http request to https://owner-api.teslamotors.com:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -132,7 +124,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { respBody, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Println("[error] TeslaMateAPICarsCommand error in second ioutil.ReadAll ", err) + log.Println("[error] TeslaMateAPICarsCommandV1 error in second ioutil.ReadAll:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } @@ -140,25 +132,15 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { // print to log about request if gin.IsDebugging() { - log.Println("[debug] TeslaMateAPICarsCommand " + command + " returned data:") + log.Println("[debug] TeslaMateAPICarsCommandV1 " + c.Request.RequestURI + " returned data:") js, _ := json.Marshal(jsonData) log.Printf("[debug] %s\n", js) } + if resp.StatusCode == http.StatusOK { + log.Println("[info] TeslaMateAPICarsCommandV1 " + c.Request.RequestURI + " executed successful.") + } else { + log.Println("[error] TeslaMateAPICarsCommandV1 " + c.Request.RequestURI + " error in execution!") + } c.JSON(resp.StatusCode, jsonData) } - -func getCommandToken() string { - // get token from environment variable COMMAND_TOKEN - token := getEnv("COMMAND_TOKEN", "") - if token == "" || len(token) < 32 { - log.Println("[warning] getCommandToken environment variable COMMAND_TOKEN not set, is empty, or too short. All POST commands will return unauthroized.") - token = "" - } - return token -} - - } else { - log.Print("[info] getAllowList COMMANDS from environment variables set, " + commandAllowListLocation + " will be ignored.") - } -} From ec69386292b8d23d4c3b2232dbe4acc98b32d6ef Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Thu, 11 Mar 2021 23:07:05 +0100 Subject: [PATCH 09/23] changing command collection to Params instead of c.Request.RequestURI --- src/v1_TeslaMateAPICarsCommand.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index 92fbb73..ff8995a 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -18,7 +18,6 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { var TeslaAccessToken, TeslaVehicleID string var jsonData map[string]interface{} var err error - var command string // check if commands are enabled.. if not we need to abort if getEnvAsBool("ENABLE_COMMANDS", false) == false { @@ -62,12 +61,16 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { return } - // I am not a fan of hardcoding "/api/v1/" - // it would be nice to find a way to retrieve api.Group - command = (c.Request.RequestURI[len("/api/v1/cars/"+ParamCarID):]) + // getting :Command + command := ("/command/" + c.Param("Command")) + // if command is /command/ or /command/wake_up, set to /wake_up only + if command == "/command/" || command == "/command/wake_up" { + command = "/wake_up" + } + log.Println("[debug] TeslaMateAPICarsCommandV1 command received:", command) - log.Print("[warning] TeslaMateAPICarsCommand command: " + command + " not allowed") if !checkArrayContainsString(allowList, command) { + log.Print("[warning] TeslaMateAPICarsCommandV1 command: " + command + " not allowed") c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) return } From 1ef9f88088f1301bfd2be3990ec9b06dedc84266 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Fri, 12 Mar 2021 09:33:41 +0100 Subject: [PATCH 10/23] adding var with API version --- src/webserver.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/webserver.go b/src/webserver.go index c8be1c2..10f8d9d 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -14,6 +14,10 @@ import ( _ "github.com/lib/pq" ) +// setting TeslaMateApi version number +// TODO: get the value from git-tag later.. +var apiVersion = "1.2.2" + // defining db var var db *sql.DB From ff442486d6099dee4e9a2a5ea3cb0f57abf1b648 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Fri, 12 Mar 2021 10:58:41 +0100 Subject: [PATCH 11/23] using http.Method instead of strings changing http.NewRequest error field to _ adding User-Agent header to http request --- src/v1_TeslaMateAPICarsCommand.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index ff8995a..be1ff1e 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -27,7 +27,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { } // if request method is GET return list of commands - if c.Request.Method == "GET" { + if c.Request.Method == http.MethodGet { c.JSON(http.StatusOK, gin.H{"enabled_commands": allowList}) return } @@ -110,9 +110,10 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { } client := &http.Client{} - req, err := http.NewRequest("POST", "https://owner-api.teslamotors.com/api/1/vehicles/"+TeslaVehicleID+command, strings.NewReader(string(reqBody))) - req.Header.Add("Authorization", "Bearer "+TeslaAccessToken) - req.Header.Add("Content-Type", "application/json") + req, _ := http.NewRequest(http.MethodPost, "https://owner-api.teslamotors.com/api/1/vehicles/"+TeslaVehicleID+command, strings.NewReader(string(reqBody))) + req.Header.Set("Authorization", "Bearer "+TeslaAccessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "TeslaMateApi/"+apiVersion+" https://github.com/tobiasehlert/teslamateapi") resp, err := client.Do(req) // check response error From 17d4cd8ca1526bba2f367aba0cda07ddba97ab44 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Fri, 12 Mar 2021 12:56:18 +0100 Subject: [PATCH 12/23] adding the Authorization Bearer support to AuthSupport --- src/AuthSupport.go | 65 ++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/src/AuthSupport.go b/src/AuthSupport.go index de81371..5d0f8f5 100644 --- a/src/AuthSupport.go +++ b/src/AuthSupport.go @@ -2,6 +2,7 @@ package main import ( "log" + "strings" "github.com/gin-gonic/gin" ) @@ -22,6 +23,36 @@ func initAuthToken() { // validateAuthToken func func validateAuthToken(c *gin.Context) (bool, string) { + // trying with http header - Authorization: Bearer + reqHeaderToken := c.Request.Header.Get("Authorization") + + // if length of reqHeaderToken is more than zero + if len(reqHeaderToken) > 0 { + + // removing Bearer part from header to get token out of header + splitToken := strings.Split(reqHeaderToken, "Bearer") + + if len(splitToken) != 2 { + // bearer token is not proper formatted.. returning bad request + log.Println("[info] validateAuthToken - header authorization bearer token is not proper formatted.. returning 401") + return false, "header authorization bearer token is not proper formatted" + + } else if strings.TrimSpace(splitToken[1]) == "" { + // bearer token is empty string.. we'll return unauthorized + log.Println("[info] validateAuthToken - header authorization bearer token is empty.. returning 401") + return false, "header authorization bearer token is empty" + + } else if checkAuthToken(strings.TrimSpace(splitToken[1])) { + // the bearer token is valid! + log.Println("[debug] validateAuthToken - header authorization bearer token valid.") + return true, "" + + } + // the check did fail.. bearer token is invalid + log.Println("[info] validateAuthToken - header authorization bearer token invalid.. returning 401") + return false, "header authorization bearer token invalid" + } + // trying with http parameter - ?token= tokenParamsValue := c.DefaultQuery("token", "") @@ -40,40 +71,6 @@ func validateAuthToken(c *gin.Context) (bool, string) { return false, "param token invalid" } - /* - // NOT fully ready I think.. - - // trying with http header - Authorization: Bearer - reqHeaderToken := c.Request.Header.Get("Authorization") - - // if length of reqHeaderToken is more than zero - if len(reqHeaderToken) > 0 { - - // removing Bearer part from header to get token out of header - splitToken := strings.Split(reqHeaderToken, "Bearer") - - if len(splitToken) != 2 { - // bearer token is not proper formatted.. returning bad request - log.Println("[info] validateAuthToken - header authorization bearer token is not proper formatted.. returning 401") - return false, "header authorization bearer token is not proper formatted" - - } else if strings.TrimSpace(splitToken[1]) == "" { - // bearer token is empty string.. we'll return unauthozied - log.Println("[info] validateAuthToken - header authorization bearer token is empty.. returning 401") - return false, "header authorization bearer token is empty" - - } else if checkAuthToken(strings.TrimSpace(splitToken[1])) { - // the bearer token is valid! - log.Println("[debug] validateAuthToken - header authorization bearer token valid.") - return true, "" - - } - // the check did fail.. bearer token is invalid - log.Println("[info] validateAuthToken - header authorization bearer token invalid.. returning 401") - return false, "header authorization bearer token invalid" - } - */ - // unauthozie all calls! return false, "failed validation" } From a060a19f6f78c1e058f301b55f09875534477aec Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Fri, 12 Mar 2021 12:57:51 +0100 Subject: [PATCH 13/23] adding documentation to README about commands and authentication fixing typo in errormessage --- README.md | 27 +++++++++++++++++++++++++++ src/v1_TeslaMateAPICarsCommand.go | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ede0433..e5bf2ab 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ TeslaMateApi is a RESTful API to get data collected by self-hosted data logger * - [Docker-compose](#docker-compose) - [Environment variables](#environment-variables) - [API documentation](#api-documentation) + - [Available endpoints](#available-endpoints) + - [Authentication](#authentication) + - [Commands](#commands) - [Security information](#security-information) - [Credits](#credits) @@ -136,6 +139,8 @@ Basically the same environment variables for the database, mqqt and timezone nee More detailed documentation of every endpoint will come.. +### Available endpoints + - GET `/api` - GET `/api/v1` - GET `/api/v1/cars` @@ -152,6 +157,28 @@ More detailed documentation of every endpoint will come.. - GET `/api/v1/globalsettings` - GET `/api/ping` +### Authentication + +If you want to use command endpoints such as `/api/v1/cars/:CarID/command/:Command` and `/api/v1/cars/:CarID/wake_up`, you need to add authentication to your request. + +You need to specify a token yourself (called **API_TOKEN**) in the environment variables file, to set it. The token has the requirement to be a minimum of 32 characters long. + +There are two options available for authentication to be done. + +1. Adding extra header `Authorization: Bearer ` to your request. (recommended option) + +2. Adding URI parameter `?token=` to the endpoint you try to reach. (not a good option) + +\* *Note: If you use the second option and your logs get compromised, your token will be leaked.* + +### Commands + +Commands are not enabled by default. You need to enable it in your environment variables file and you need to specify which commands you want to use as well. + +A list of possible commands can be found under [environment variables](#environment-variables). + +Regarding what fields you need to provide in the commands, we will referr to the [timdorr/tesla-api](https://tesla-api.timdorr.com/vehicle/commands) documentation. + ## Security information There is **no** possibility to get access to your Tesla account tokens by this API and we'll keep it this way! diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index be1ff1e..ffc5f46 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -71,7 +71,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { if !checkArrayContainsString(allowList, command) { log.Print("[warning] TeslaMateAPICarsCommandV1 command: " + command + " not allowed") - c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthroized"}) + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } From fb2a465452f6e751ef745350b7de38b1b93f351a Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Fri, 12 Mar 2021 15:45:00 -0600 Subject: [PATCH 14/23] additional commands documentation, allow_list.json example --- README.md | 6 +++++- example/allow_list.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 example/allow_list.json diff --git a/README.md b/README.md index e5bf2ab..08fde57 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,11 @@ There are two options available for authentication to be done. ### Commands -Commands are not enabled by default. You need to enable it in your environment variables file and you need to specify which commands you want to use as well. +Commands are not enabled by default. You need to enable them in your environment variables and you need to specify which commands you want to use as well. + +The most coarse option `COMMANDS_ALL=true` will enable all commands. Specific groups of commands can be enabled for example `COMMANDS_ALERT=true` will enable the [alert](https://tesla-api.timdorr.com/vehicle/commands/alerts) commands group. If you need a granular set of commands enabled `COMMANDS_ALLOWLIST=/path/to/allow_list.json` can be used to specify a [JSON formatted list of commands](./example/allow_list.json) to enable. + +*note: if `COMMANDS_ALL` or any specific group of commands has been enabled `COMMANDS_ALLOWLIST` is ignored. A list of possible commands can be found under [environment variables](#environment-variables). diff --git a/example/allow_list.json b/example/allow_list.json new file mode 100644 index 0000000..f521cf0 --- /dev/null +++ b/example/allow_list.json @@ -0,0 +1 @@ +["/command/wake_up","/command/flash_lights"] From 78cf8850b55b545b722350cd4cb8469fe45121cd Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Tue, 16 Mar 2021 12:28:29 +0100 Subject: [PATCH 15/23] adjusting allow_list.json example.. /command/wake_up to /wake_up --- example/allow_list.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/allow_list.json b/example/allow_list.json index f521cf0..11817a1 100644 --- a/example/allow_list.json +++ b/example/allow_list.json @@ -1 +1 @@ -["/command/wake_up","/command/flash_lights"] +["/wake_up","/command/flash_lights"] From 82e48022d8839cdd18d24da0476d15f56a8ebd37 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Tue, 16 Mar 2021 12:34:05 +0100 Subject: [PATCH 16/23] updating README a little --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 08fde57..712a4f5 100644 --- a/README.md +++ b/README.md @@ -173,11 +173,19 @@ There are two options available for authentication to be done. ### Commands -Commands are not enabled by default. You need to enable them in your environment variables and you need to specify which commands you want to use as well. +Commands are not enabled by default. -The most coarse option `COMMANDS_ALL=true` will enable all commands. Specific groups of commands can be enabled for example `COMMANDS_ALERT=true` will enable the [alert](https://tesla-api.timdorr.com/vehicle/commands/alerts) commands group. If you need a granular set of commands enabled `COMMANDS_ALLOWLIST=/path/to/allow_list.json` can be used to specify a [JSON formatted list of commands](./example/allow_list.json) to enable. +You need to enable them in your environment variables (with `ENABLE_COMMANDS=true`) and you need to specify which commands you want to use as well. -*note: if `COMMANDS_ALL` or any specific group of commands has been enabled `COMMANDS_ALLOWLIST` is ignored. +There are 3 ways of using Commands: + +1. Specific groups of commands can be enabled for example `COMMANDS_ALERT=true` will enable the [alert](https://tesla-api.timdorr.com/vehicle/commands/alerts) commands group. + +2. If you need a granular set of commands enabled `COMMANDS_ALLOWLIST=/path/to/allow_list.json` can be used to specify a [JSON formatted list of commands](./example/allow_list.json) to enable. + +3. The most coarse option `COMMANDS_ALL=true` will enable all commands (specific groups and allow_list will be ignored). + +\* *Note: if `COMMANDS_ALL` or any specific group of commands has been enabled `COMMANDS_ALLOWLIST` is ignored.* A list of possible commands can be found under [environment variables](#environment-variables). From 4d4f97ee016df224ede377308edc66de163a4c46 Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Mon, 22 Mar 2021 19:43:57 -0500 Subject: [PATCH 17/23] enable COMMANDS_ALLOWLIST --- src/CommandSupport.go | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/CommandSupport.go b/src/CommandSupport.go index c18a812..8151331 100644 --- a/src/CommandSupport.go +++ b/src/CommandSupport.go @@ -1,7 +1,10 @@ package main import ( + "encoding/json" + "io/ioutil" "log" + "os" "strings" "github.com/gin-gonic/gin" @@ -125,34 +128,30 @@ func initCommandAllowList() { "/command/cancel_software_update") } - /* - // TODO: add back COMMANDS_ALLOWLIST check.. - - // if allowList is empty, read COMMANDS_ALLOWLIST and append to allowList - commandAllowListLocation := getEnv("COMMANDS_ALLOWLIST", "allow_list.json") - if len(allowList) == 0 { - var allowListFile []string - commandAllowListFile, err := os.Open(commandAllowListLocation) + // if allowList is empty, read COMMANDS_ALLOWLIST and append to allowList + commandAllowListLocation := getEnv("COMMANDS_ALLOWLIST", "allow_list.json") + if len(allowList) == 0 { + var allowListFile []string + commandAllowListFile, err := os.Open(commandAllowListLocation) + if err != nil { + log.Println("[error] getAllowList error with COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") + } else { + byteValue, err := ioutil.ReadAll(commandAllowListFile) if err != nil { - log.Println("[error] getAllowList error with COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") + log.Println("[error] getAllowList error while reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") } else { - byteValue, err := ioutil.ReadAll(commandAllowListFile) + err = json.Unmarshal(byteValue, &allowListFile) if err != nil { - log.Println("[error] getAllowList error while reading COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") + log.Println("[error] getAllowList error while parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") } else { - err = json.Unmarshal(byteValue, &allowListFile) - if err != nil { - log.Println("[error] getAllowList error while parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") - } else { - allowList = append(allowList, allowListFile...) - commandAllowListFile.Close() - } + allowList = append(allowList, allowListFile...) + commandAllowListFile.Close() } } - } else { - log.Print("[info] getAllowList COMMANDS from environment variables set, " + commandAllowListLocation + " will be ignored.") } - */ + } else { + log.Print("[info] getAllowList COMMANDS from environment variables set, " + commandAllowListLocation + " will be ignored.") + } if gin.IsDebugging() { log.Println("[info] initCommandAllowList - generated following list of allowed commands: " + strings.Join(allowList, ", ")) From 8479e0e36fd59fb9a19234a24cd100e7f8d4f265 Mon Sep 17 00:00:00 2001 From: Leland Sindt Date: Mon, 22 Mar 2021 19:46:02 -0500 Subject: [PATCH 18/23] defer commandAllowList close. --- src/CommandSupport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommandSupport.go b/src/CommandSupport.go index 8151331..685af04 100644 --- a/src/CommandSupport.go +++ b/src/CommandSupport.go @@ -133,6 +133,7 @@ func initCommandAllowList() { if len(allowList) == 0 { var allowListFile []string commandAllowListFile, err := os.Open(commandAllowListLocation) + defer commandAllowListFile.Close() if err != nil { log.Println("[error] getAllowList error with COMMANDS_ALLOWLIST: " + commandAllowListLocation + " not found and will be ignored") } else { @@ -145,7 +146,6 @@ func initCommandAllowList() { log.Println("[error] getAllowList error while parsing JSON.. COMMANDS_ALLOWLIST: " + commandAllowListLocation + " it will be ignored") } else { allowList = append(allowList, allowListFile...) - commandAllowListFile.Close() } } } From 7fb964a67f63aa967fc93b973b3046449e2313fc Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Wed, 24 Mar 2021 13:52:35 +0100 Subject: [PATCH 19/23] adjusting user-agent for post action --- src/v1_TeslaMateAPICarsCommand.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v1_TeslaMateAPICarsCommand.go b/src/v1_TeslaMateAPICarsCommand.go index ffc5f46..4193500 100644 --- a/src/v1_TeslaMateAPICarsCommand.go +++ b/src/v1_TeslaMateAPICarsCommand.go @@ -113,7 +113,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) { req, _ := http.NewRequest(http.MethodPost, "https://owner-api.teslamotors.com/api/1/vehicles/"+TeslaVehicleID+command, strings.NewReader(string(reqBody))) req.Header.Set("Authorization", "Bearer "+TeslaAccessToken) req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "TeslaMateApi/"+apiVersion+" https://github.com/tobiasehlert/teslamateapi") + req.Header.Set("User-Agent", "TeslaMateApi/"+apiVersion+" (+https://github.com/tobiasehlert/teslamateapi)") resp, err := client.Do(req) // check response error From cde4a0f3ac79911e7849d4688c85f9e92a3e652d Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Wed, 24 Mar 2021 13:52:54 +0100 Subject: [PATCH 20/23] updating version number in code (to 1.3.1) --- src/webserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webserver.go b/src/webserver.go index 10f8d9d..97e8ed7 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -16,7 +16,7 @@ import ( // setting TeslaMateApi version number // TODO: get the value from git-tag later.. -var apiVersion = "1.2.2" +var apiVersion = "1.3.1" // defining db var var db *sql.DB From b9b1bcdfc13a62558c6560c918aa22a5f26901df Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Wed, 24 Mar 2021 14:16:28 +0100 Subject: [PATCH 21/23] typo --- src/webserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webserver.go b/src/webserver.go index 97e8ed7..0cfd6de 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -110,7 +110,7 @@ func main() { api.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "pong"}) }) } - // TeslaMateApi endpoints (bofore versioning) + // TeslaMateApi endpoints (before versioning) r.GET("/cars", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1"+c.Request.RequestURI) }) r.GET("/cars/:CarID", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1"+c.Request.RequestURI) }) r.GET("/cars/:CarID/charges", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1"+c.Request.RequestURI) }) From b5fff58bcc15f2f08158dd42597d88d8f3bde9e9 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Wed, 24 Mar 2021 14:16:52 +0100 Subject: [PATCH 22/23] in case someone accesses GET command with commands --- src/webserver.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webserver.go b/src/webserver.go index 0cfd6de..4edc8d5 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -87,6 +87,7 @@ func main() { // v1 /api/v1/cars/:CarID/command endpoints v1.GET("/cars/:CarID/command", TeslaMateAPICarsCommandV1) + v1.GET("/cars/:CarID/commands", TeslaMateAPICarsCommandV1) v1.POST("/cars/:CarID/command/:Command", TeslaMateAPICarsCommandV1) // v1 /api/v1/cars/:CarID/drives endpoints From 4f9d533b6abd8f6de890f5766107589100f8a9f1 Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Thu, 25 Mar 2021 09:27:32 +0100 Subject: [PATCH 23/23] bumping new version v1.4.0 updating CHANGELOG --- CHANGELOG.md | 13 ++++++++++--- src/webserver.go | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b833a9c..113dff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ## [Unreleased] +## [1.4.0] - 2021-03-25 + +### Added +- added feature commands to proxy POST commands to Tesla owner API (#22) +- support for authentication on command endpoints + ## [1.3.1] - 2021-03-23 -### Changed +### Fixed - fixing sql error when BatteryHeaterNoPower is null (#19 by @LelandSindt) ## [1.3.0] - 2021-03-17 @@ -90,8 +96,9 @@ ## [1.0.0] - 2021-02-15 -[Unreleased]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.1...HEAD -[1.3.1]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.1...v1.3.1 +[Unreleased]: https://github.com/tobiasehlert/teslamateapi/compare/v1.4.0...HEAD +[1.3.1]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.1...v1.4.0 +[1.3.1]: https://github.com/tobiasehlert/teslamateapi/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/tobiasehlert/teslamateapi/compare/v1.2.3...v1.3.0 [1.2.3]: https://github.com/tobiasehlert/teslamateapi/compare/v1.2.2...v1.2.3 [1.2.2]: https://github.com/tobiasehlert/teslamateapi/compare/v1.2.1...v1.2.2 diff --git a/src/webserver.go b/src/webserver.go index 4edc8d5..d3f35ad 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -16,7 +16,7 @@ import ( // setting TeslaMateApi version number // TODO: get the value from git-tag later.. -var apiVersion = "1.3.1" +var apiVersion = "1.4.0" // defining db var var db *sql.DB