packages feed

rails-session 0.1.1.0 → 0.1.2.0

raw patch · 4 files changed

+202/−4 lines, 4 filesdep +base16-bytestringdep +containersdep +semigroupsdep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: base16-bytestring, containers, semigroups

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

+ Web.Rails3.Session: Cookie :: ByteString -> Cookie
+ Web.Rails3.Session: Secret :: ByteString -> Secret
+ Web.Rails3.Session: decode :: Secret -> Cookie -> Maybe RubyObject
+ Web.Rails3.Session: decodeEither :: Secret -> Cookie -> Either String RubyObject
+ Web.Rails3.Session: lookupUserIds :: (Num a) => RubyObject -> Maybe (NonEmpty a)
+ Web.Rails3.Session: newtype Cookie
+ Web.Rails3.Session: newtype Secret

Files

CHANGELOG.md view
@@ -1,3 +1,7 @@+## 0.1.2.0++- Add Rails3 support.+ ## 0.1.1.0  - Export DecryptedData type.
rails-session.cabal view
@@ -1,5 +1,5 @@ name:                rails-session-version:             0.1.1.0+version:             0.1.2.0 synopsis:            Decrypt Ruby on Rails sessions in Haskell description:         Please see README.md homepage:            http://github.com/iconnect/rails-session#readme@@ -20,6 +20,7 @@ library   hs-source-dirs:      src   exposed-modules:     Web.Rails.Session+                     , Web.Rails3.Session   build-depends:       base              >= 4.7 && < 5                      , base-compat       >= 0.8.2                      , base64-bytestring >= 1.0.0.1@@ -30,6 +31,11 @@                      , ruby-marshal      >= 0.1.1                      , string-conv       >= 0.1                      , vector            >= 0.10.12.3+                     , bytestring+                     , base16-bytestring+                     , containers+  if impl(ghc < 8.0)+     build-depends: semigroups   default-language:    Haskell2010  test-suite specs@@ -40,6 +46,7 @@   build-depends:       base                      , bytestring                      , filepath+                     , semigroups                      , rails-session                      , ruby-marshal                      , tasty
+ src/Web/Rails3/Session.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Web.Rails3.Session+  (+    -- * Tutorial+    -- $tutorial++    -- * Decoding+    decodeEither+  , decode+    -- * Utilities+  , lookupUserIds+    -- * Throw-away data-types+    -- $datatypes+  , Secret(..)+  , Cookie(..)+  )+where++import           Crypto.Hash                  as Hash+import           Crypto.MAC.HMAC              as HMAC+import           Data.ByteString+import           Data.ByteString              as BS+import qualified Data.ByteString.Base16       as B16+import qualified Data.ByteString.Base64       as B64+import qualified Data.Map.Strict              as Map+import           Data.Ruby.Marshal            as Marshal hiding (decode, decodeEither)+import qualified Data.Ruby.Marshal            as Marshal (decodeEither)+import           Data.Ruby.Marshal.RubyObject+import Network.HTTP.Types (urlDecode)+import Data.List.NonEmpty as NE+import Data.List as DL+import Prelude (Either(..), (>>=), (.), (==), ($), Maybe(..), return, Num(..), Int, fromIntegral, Bool(..), fst, String, either, id, const)++-- $tutorial+--+-- Here's how to decode a Rail3 session/auth cookie using 'wai' & 'cookie' package.+--+-- @+-- import Network.Wai (requestHeaders)+-- import Web.Cookie (parseCookies)+-- ...+--+-- case (fmap (lookup "_yourapp_session") $ fmap parseCookies $ lookup "Cookie" $ requestHeaders waiRequest) of+--+-- -- no active Rails session+-- Left _ -> ...+--+-- Right c -> case (decodeEither (Secret "yourSessionSecret") (Cookie c)) of+--+--   -- something went wrong in decoding the cookie. You should log "e" for debugging!+--   Left e -> ...+--+--   Right obj -> case (lookupUserIds obj) of+--+--      -- we have a Rails session-cookie, but the user has not signed-in+--     Nothing -> ...+--+--     -- signed-in user. This /may/ contain muliple userIds depending up on how you have configured Devise\/Warden in your Rails app.+--     Just userIds -> ...+-- @++-- $datatypes+--+-- These data-types exist only as a way to semantically differentiate between+-- various ByteString arguments when they are passed to functions. This is required+-- only because Haskell doesn't have proper keywords-arguments.++newtype Secret = Secret ByteString+newtype Cookie = Cookie ByteString++maybeToEither :: a -> Maybe b -> Either a b+maybeToEither _ (Just b) = Right b+maybeToEither a Nothing = Left a++-- | Decode a cookie encoded by Rails3. Please read the documentation of+-- 'decodeEither' for more details, and consider using 'decodeEither' instead of+-- 'decode'+decode :: Secret -> Cookie -> Maybe RubyObject+decode s c = either (const Nothing) Just $ decodeEither s c++-- | Decode a cookie encoded by Rails3. You can find the @Secret@ in a file+-- called @config\/initializers\/secret_token.rb@ in your Rail3 app.+--+-- __Note:__ `decodeMaybe` has not been added on purpose. When cookie decoding+-- fails, you would really want to know why. Please consider logging `Left`+-- values returned by this function in your log, to save yourself some debugging+-- time later.+decodeEither :: Secret -> Cookie -> Either String RubyObject+decodeEither (Secret cookieSecret) (Cookie x) =+  extractChecksum+  >>= compareChecksum+  >>= Marshal.decodeEither+  where+    extractChecksum :: Either String (Digest SHA1)+    extractChecksum = maybeToEither+                      "[Rails3 Cookie] Illegal checksum in cookie. Wasn't able to extract a valid HMAC checksum out of it."+                      (Hash.digestFromByteString $ fst $ (B16.decode hexChecksum))++    compareChecksum :: Digest SHA1 -> Either String ByteString+    compareChecksum checksum = if (computedChecksum == checksum) then (Right $ B64.decodeLenient b64) else (Left "[Rails3 Cookie] Checksum doesn't match")++    computedChecksum :: Digest SHA1+    computedChecksum = HMAC.hmacGetDigest (HMAC.hmac cookieSecret b64 :: HMAC SHA1)++    (b64, hexChecksum) = let (a, b) = (breakSubstring delimiter $ urlDecode False x)+                         in (a, BS.drop (BS.length delimiter) b)+    delimiter = "--"+++-- NOTE: Please refer to+-- http://blog.bigbinary.com/2013/03/19/cookies-on-rails.html to understand how+-- a Rails3 cookie is encoded (NOT encyrpted). Encryption of session cookies+-- only began in Rails4. Rails3 marshals a RubyObject and base64 encodes it to+-- store it as a cookie. To ensure that it cannot be tamped with, it also adds+-- an HMAC computed with the help of a secret key/value/token.++safeHead :: [a] -> Maybe a+safeHead []    = Nothing+safeHead (x:_) = Just x++lookupKey :: (Rubyable a) => (BS.ByteString, RubyStringEncoding) -> RubyObject -> Maybe a+lookupKey key robj = (fromRuby robj :: Maybe (Map.Map (BS.ByteString, RubyStringEncoding) RubyObject))+  >>= Map.lookup key+  >>= fromRuby++-- | Lookup the Warden\/Devise UserIds from a decoded cookie. __Please note,__ a+-- cookie may contain multiple UserIds, because it /seems/ that it is possible+-- to be logged-in as multiple users simultaneously, if you define [multiple+-- user+-- models](https://github.com/plataformatec/devise/wiki/How-to-Setup-Multiple-Devise-User-Models)+-- (the underlying data-structure allows it, as well).+lookupUserIds :: (Num a) => RubyObject -> Maybe (NonEmpty a)+lookupUserIds robj =+  lookupKey ("warden.user.user.key", UTF_8) robj+  >>= (\x -> fromRuby x :: Maybe [RubyObject]) -- [[int, int int], "random string"]+  >>= safeHead+  >>= (\x -> fromRuby x :: Maybe [Int]) -- [int, int, int]+  >>= (\xs -> return $ DL.map fromIntegral xs)+  >>= NE.nonEmpty
test/Spec.hs view
@@ -11,14 +11,61 @@ import           Test.Tasty (defaultMain, testGroup) import           Test.Tasty.Hspec (describe, it, shouldBe, shouldSatisfy, testSpec, Spec) import           Web.Rails.Session+import qualified Web.Rails3.Session as R3+import qualified Data.List.NonEmpty as NE  -- SPECS  main :: IO () main = do   rails4 <- testSpec "Web.Rails.Session: Rails4" $ specsFor Rails4-  defaultMain (testGroup "All the Specs" [rails4])+  rails3 <- testSpec "Web.Rails3.Session: Rails4" specsForRails3+  defaultMain (testGroup "All the Specs" [rails4, rails3]) +specsForRails3 :: Spec+specsForRails3 = do+  let cookie = R3.Cookie $ unsafePerformIO $ BS.readFile "test/Rails3"+      secret_ = R3.Secret "test secret token for testing cookies"+      sessionIdVal_ = "3e0d672d2d594d02c8f015388ae380a8"+      csrfTokenVal_ = "fKui2MZV3u5jhGBIgvvrOi7lo0mD/Jge3e0LOAS19Vg="+      userIdVal_ = 107 :: Int+      wardenContents_ = RArray (fromList [RArray (fromList [RFixnum userIdVal_]), RIVar (RString "$2a$10$hDu7wHneNw4A.6Wg61R4vO",UTF_8)])+      cookieContents_ = RHash (fromList+                          [ (RIVar (RString "session_id",UTF_8), RIVar (RString sessionIdVal_,UTF_8))+                          , (RIVar (RString "warden.user.user.key",UTF_8), wardenContents_)+                          , (RIVar (RString "_csrf_token",US_ASCII),RIVar (RString csrfTokenVal_,US_ASCII))+                          ])+++  describe "all tests" $ do+    it "should be a Right(..)" $ do+      let result = R3.decodeEither secret_ cookie+      result `shouldSatisfy` isRight++    it "should be a fully-formed Ruby object" $ do+      case R3.decodeEither secret_ cookie of+        Left e -> error $ "decodeEither failed:" ++ (show e)+        Right result -> do+          result `shouldBe` cookieContents_++    it "should look up the '_csrf_token'" $ do+      case R3.decodeEither secret_ cookie of+        Left e -> error $ "decodeEither failed:" ++ (show e)+        Right result ->+          csrfToken result `shouldBe` Just csrfTokenVal_++    it "should look up the 'session_id'" $ do+      case R3.decodeEither secret_ cookie of+        Left e -> error $ "decodeEither failed:" ++ (show e)+        Right result ->+          sessionId result `shouldBe` Just sessionIdVal_++    it "should look up the warden user_id(s)" $ do+      case R3.decodeEither secret_ cookie of+        Left e -> error $ "decodeEither failed:" ++ (show e)+        Right result ->+          R3.lookupUserIds result `shouldBe` Just (userIdVal_ NE.:| [] :: NE.NonEmpty Int)+ specsFor :: Rails -> Spec specsFor rails = do   let cookie = unsafeReadCookie rails@@ -33,7 +80,6 @@         Left _ -> error "decode failed"         Right result -> do           result `shouldBe` rubySession-   describe "decrypt" $ do     it "should be a Right(..)" $ do       let result = decrypt Nothing secret cookie@@ -90,7 +136,7 @@  -- UTIL -data Rails = Rails4 deriving (Show)+data Rails = Rails4 | Rails3 deriving (Show)  unsafeReadCookie :: Rails -> Cookie unsafeReadCookie rails = unsafePerformIO $