packages feed

tesla 0.6.0.0 → 0.7.0.0

raw patch · 4 files changed

+27/−106 lines, 4 files

Files

ChangeLog.md view
@@ -1,5 +1,17 @@ # Changelog for tesla +# Release 0.7.0.0++Authentication is no longer supported.++It hasn't worked for about a year now and Tesla aggressively thwarts automation.++# Release 0.6.0.0++All APIs that send data use JSON now.++Previously we sent form/data and that was fine, but apparently no longer is.+ # Release 0.5.0.0  Added:
README.md view
@@ -3,3 +3,9 @@ Tesla API for Haskell.  See also: https://tesla-api.timdorr.com/++## Authentication++This library used to support authenticating via username and password to get credentials, but Tesla busted that pretty hard.++The best practice at this point is to use something like https://www.teslafi.com/tokenUser.php to get a bearer and refresh token and feed them to the library via `fromToken` and `refreshAuth`
src/Tesla.hs view
@@ -24,134 +24,37 @@     ) where  -import           Control.Exception          (catch) import           Control.Lens-import           Control.Monad              (when)-import           Control.Monad.Catch        (SomeException) import           Control.Monad.IO.Class     (MonadIO (..))-import           Control.Retry              (defaultLogMsg, exponentialBackoff, limitRetries, logRetries, recovering)-import           Crypto.Hash                (SHA256 (..), hashWith) import           Data.Aeson                 (FromJSON, Value (..), encode) import           Data.Aeson.Lens            (_Array, _Double, _Integer, _String, key)-import qualified Data.ByteArray             as BA-import qualified Data.ByteString            as BS-import qualified Data.ByteString.Base64.URL as B64-import qualified Data.ByteString.Char8      as BC-import qualified Data.ByteString.Lazy       as BL import           Data.Foldable              (asum) import           Data.Map.Strict            (Map) import qualified Data.Map.Strict            as Map import           Data.Maybe                 (catMaybes) import           Data.Text                  (Text) import qualified Data.Text                  as T-import qualified Data.Text.Encoding         as TE-import           Network.HTTP.Client        (HttpException (..), HttpExceptionContent (TooManyRedirects))-import           Network.Wreq               (FormParam (..), Options, asJSON, checkResponse, defaults, header,-                                             hrFinalResponse, params, redirects, responseBody, responseHeader)-import qualified Network.Wreq.Session       as Sess-import           System.Random-import           Text.HTML.TagSoup          (fromAttrib, isTagOpenName, parseTags)+import           Network.Wreq               (Options, defaults, header)  import           Tesla.Auth import           Tesla.Internal.HTTP  baseURL :: String baseURL =  "https://owner-api.teslamotors.com/"-authURL :: String-authURL = "https://auth.tesla.com/oauth2/v3/authorize"-authTokenURL :: String-authTokenURL = "https://owner-api.teslamotors.com/oauth/token" authRefreshURL :: String authRefreshURL = "https://auth.tesla.com/oauth2/v3/token" productsURL :: String productsURL = baseURL <> "api/1/products" --- | Authenticate to the Tesla service.-authenticate :: AuthInfo -> IO AuthResponse-authenticate ai = recovering policy [retryOnAnyStatus] $ \_ -> do-  sess <- Sess.newSession-  verifier <- BS.pack . take 86 . randoms <$> getStdGen-  state <- clean64 . BS.pack . take 16 . randoms <$> getStdGen-  authenticate' sess verifier state ai--  where-    policy = exponentialBackoff 2000000 <> limitRetries 9-    retryOnAnyStatus = logRetries retryOnAnyError reportError-    retryOnAnyError :: SomeException -> IO Bool-    retryOnAnyError _ = pure True-    reportError retriedOrCrashed err retryStatus = putStrLn $ defaultLogMsg retriedOrCrashed err retryStatus--clean64 :: BC.ByteString -> Text-clean64 = TE.decodeUtf8 . BS.reverse . BS.dropWhile (== 61) . BS.reverse . B64.encode--authenticate' :: Sess.Session -> BC.ByteString -> Text -> AuthInfo -> IO AuthResponse-authenticate' sess verifier state ai@AuthInfo{..} = do-  -- 1. First, grab the form.-  form <- formFields . BL.toStrict . view responseBody <$> Sess.getWith (aOpts & params .~ gparams) sess authURL-  -- There are required hidden fields -- if we didn't get them, we got the wrong http response-  when (null form) $ fail "tesla didn't return login form"--  -- 2. Now we post the form with all of our credentials.-  let form' = form <> ["identity" := _email, "credential" := _password]-  Just code <- fmap (xcode . BC.unpack) <$> findRedirect authURL (fopts-                                                                  & params .~ gparams-                                                                  & redirects .~ 0-                                                                  & checkResponse ?~ (\_ _ -> pure ())-                                                                 ) form'-  -- Extract the "code" from the URL we were redirected to... we can't actually follow the redirect :/-  let jreq = encode $ Object (mempty-                               & at "grant_type" ?~ "authorization_code"-                               & at "client_id" ?~ "ownerapi"-                               & at "code" ?~ String code-                               & at "code_verifier" ?~ String verifierHash-                               & at "redirect_uri" ?~ "https://auth.tesla.com/void/callback")--  -- 3. Posting that code and other junk back to the token URL gets us temporary credentials.-  ar <- view responseBody <$> (Sess.postWith jOpts sess authRefreshURL jreq >>= asJSON)-  translateCreds ai ar--  where-    verifierHash = clean64 . BS.pack . BA.unpack $ hashWith SHA256 verifier-    gparams = [("client_id", "ownerapi"),-                 ("code_challenge", verifierHash),-                 ("code_challenge_method", "S256"),-                 ("redirect_uri", "https://auth.tesla.com/void/callback"),-                 ("response_type", "code"),-                 ("scope", "openid email offline_access"),-                 ("state", state)]-    -- extract all the non-empty form fields from an HTML response-    formFields = map (\t -> fromAttrib "name" t := fromAttrib "value" t)-                 . filter (\t -> isTagOpenName "input" t && fromAttrib "value" t /= "")-                 . parseTags-    fopts = aOpts & header "content-type" .~ ["application/x-www-form-urlencoded"]-    xcode u = head [v | q <- tail (T.splitOn "?" (T.pack u)),-                        kv <- T.splitOn "&" q,-                        (k,v) <- paird (T.splitOn "=" kv),-                        k == "code"]-    paird [a,b] = [(a,b)]-    paird _     = []--    findRedirect u opts a = preview (_Just . responseHeader "Location") <$> (inBody `catch` inException)-      where-        inBody = preview hrFinalResponse <$> Sess.customHistoriedPayloadMethodWith "POST" opts sess u a-        inException (HttpExceptionRequest _ (TooManyRedirects (r:_))) = pure (Just r)---translateCreds :: AuthInfo -> AuthResponse -> IO AuthResponse-translateCreds AuthInfo{..} AuthResponse{..} = do-  -- 4. And we finally get the useful credentials by exchanging the temporary credentials.-  let jreq2 = encode $ Object (mempty-                               & at "grant_type" ?~ "urn:ietf:params:oauth:grant-type:jwt-bearer"-                               & at "client_id" ?~ String (T.pack _clientID)-                              )--  ar <- jpostWith (jOpts & header "Authorization" .~ ["bearer " <> BC.pack _access_token]) authTokenURL jreq2-  pure (ar & refresh_token .~ _refresh_token) -- replace the refresh token with the one from step 3+{-# DEPRECATED authenticate "Tesla busted authentication pretty hard.  See https://github.com/dustin/tesla for more info." #-} +-- | Fail to authenticate to the Tesla service.+authenticate :: AuthInfo -> IO AuthResponse+authenticate _ = fail "Tesla busted authentication pretty hard.  See https://github.com/dustin/tesla for more info."  -- | Refresh authentication credentials using a refresh token.-refreshAuth :: AuthInfo -> AuthResponse -> IO AuthResponse-refreshAuth _ AuthResponse{..} = do+refreshAuth :: AuthResponse -> IO AuthResponse+refreshAuth AuthResponse{..} = do   jpostWith jOpts authRefreshURL (encode $ Object (mempty                                                          & at "grant_type" ?~ "refresh_token"                                                          & at "client_id" ?~ "ownerapi"
tesla.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 44f2c38345f7a71e96cf36e8d5f45c3c741ba014fb6122e6d9ca4b3e136d0d0f+-- hash: 1725c4641e78097961719f44569b569c7e52a82a027e84d8d32cdf24c01ae3b0  name:           tesla-version:        0.6.0.0+version:        0.7.0.0 synopsis:       Tesla API client. description:    Please see the README on GitHub at <https://github.com/dustin/tesla#readme> category:       Web