packages feed

ripple-federation (empty) → 0.1

raw patch · 5 files changed

+333/−0 lines, 5 filesdep +aesondep +attoparsecdep +basesetup-changed

Dependencies added: aeson, attoparsec, base, base58address, blaze-builder, bytestring, errors, http-streams, http-types, io-streams, network, text, unexceptionalio

Files

+ COPYING view
@@ -0,0 +1,13 @@+Copyright © 2014, Stephen Paul Weber <singpolyma.net>++Permission to use, copy, modify, and/or distribute this software for any+purpose with or without fee is hereby granted, provided that the above+copyright notice and this permission notice appear in all copies.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ README view
@@ -0,0 +1,1 @@+To resolve federation names, etc.
+ Ripple/Federation.hs view
@@ -0,0 +1,273 @@+module Ripple.Federation (+	resolve,+	getRippleTxt,+	Alias(..),+	ResolvedAlias(..),+	-- * Errors+	Error(..),+	ErrorType(..),+	-- * Utils+	rippleTxtParser+)where++import Control.Applicative ((<$>), (<*>), (*>), (<*), (<|>), many, some)+import Control.Monad (guard)+import Data.Either (rights)+import Data.Monoid (Monoid(..))+import Data.Word (Word32)+import Control.Error (readZ, fmapLT, throwT, runEitherT, EitherT(..), hoistEither, note)+import UnexceptionalIO (fromIO, runUnexceptionalIO, UnexceptionalIO)+import Control.Exception (fromException)+import Data.Base58Address (RippleAddress)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS8 -- eww++import Blaze.ByteString.Builder (Builder)+import Network.URI (URI(..), URIAuth(..), parseAbsoluteURI)+import System.IO.Streams (OutputStream, InputStream)+import System.IO.Streams.Attoparsec (parseFromStream, ParseException(..))+import Network.Http.Client (withConnection, establishConnection, sendRequest, buildRequest, http, setAccept, Response, receiveResponse, RequestBuilder, setContentLength)+import qualified Network.Http.Client as HttpStreams++import Network.HTTP.Types.QueryLike (QueryLike(..))+import Network.HTTP.Types.URI (renderQuery)+import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), (.:?))+import qualified Data.Aeson as Aeson+import qualified Data.Attoparsec.ByteString as Attoparsec+import qualified Data.Attoparsec.ByteString.Char8 as Attoparsec hiding (takeTill)++-- * Errors and stuff++data ErrorType = NoSuchUser | NoSupported | NoSuchDomain | InvalidParams | Unavailable deriving (Show, Eq)++data Error = Error {+		errorType :: ErrorType,+		errorMessage :: Text+	} deriving (Show, Eq)++instance Monoid Error where+	mempty = Error Unavailable (T.pack "mempty")+	mappend _ y = y++instance ToJSON Error where+	toJSON (Error typ message) = object [+			T.pack "result" .= "error",+			T.pack "error" .= typ,+			T.pack "error_message" .= message+		]++instance FromJSON Error where+	parseJSON (Aeson.Object o) =+		Error                 <$>+		(o .: T.pack "error") <*>+		(o .: T.pack "error_message")+	parseJSON _ = fail "Ripple federation errors are always objects."++instance ToJSON ErrorType where+	toJSON NoSuchUser = toJSON "noSuchUser"+	toJSON NoSupported = toJSON "noSupported"+	toJSON NoSuchDomain = toJSON "noSuchDomain"+	toJSON InvalidParams = toJSON "invalidParams"+	toJSON Unavailable = toJSON "unavailable"++instance FromJSON ErrorType where+	parseJSON (Aeson.String s) =+		maybe (fail "Unknown Ripple federation error type.") return $+		lookup s [+			(T.pack "noSuchUser", NoSuchUser),+			(T.pack "noSupported", NoSupported),+			(T.pack "noSuchDomain", NoSuchDomain),+			(T.pack "invalidParams", InvalidParams),+			(T.pack "unavailable", Unavailable)+		]+	parseJSON _ = fail "Ripple federation error type is always a string."++newtype FederationResult a = FederationResult (Either Error a)++instance (FromJSON a) => FromJSON (FederationResult a) where+	parseJSON v@(Aeson.Object o) = FederationResult <$> do+		r <- o .:? T.pack "result"+		case r of+			Just x | x == T.pack "error" -> Left <$> Aeson.parseJSON v+			_ -> Right <$> Aeson.parseJSON v+	parseJSON _ = fail "Ripple federation results are always objects."++-- * Aliases: user@domain.tld++-- | destination\@domain+data Alias = Alias {+		destination :: Text,+		domain      :: Text+	} deriving (Eq)++instance Show Alias where+	show (Alias dest domain) = T.unpack dest ++ "@" ++ T.unpack domain++instance Read Alias where+	readsPrec _ = readParen False go+		where+		domainchars = ['a'..'z']++['A'..'Z']++['0'..'9']++['-','.']+		whitespace = [' ', '\t', '\n', '\r']+		go s = case span (/='@') (dropWhile (`elem` whitespace) s) of+			(dest, '@':rest) ->+				let (domain, end) = span (`elem` domainchars) rest in+				[(Alias (T.pack dest) (T.pack domain), end)]+			_ -> []++instance QueryLike Alias where+	toQuery (Alias dest domain) = toQuery [+			("type", T.pack "federation"),+			("destination", dest),+			("user", dest),+			("domain", domain)+		]++data ResolvedAlias = ResolvedAlias {+		alias  :: Alias,+		ripple :: RippleAddress,+		dt     :: Maybe Word32+	} deriving (Show, Eq)++instance ToJSON ResolvedAlias where+	toJSON (ResolvedAlias (Alias dest domain) ripple dt) = object [+		T.pack "federation_json" .= object ([+				T.pack "type" .= "federation_record",+				T.pack "destination" .= dest,+				T.pack "domain" .= domain,+				T.pack "destination_address" .= show ripple+			] ++ maybe [] (\x -> [T.pack "dt" .= x]) dt)+		]++instance FromJSON ResolvedAlias where+	parseJSON (Aeson.Object o) = do+		o' <- o .: T.pack "federation_json"+		dest <- o' .:? T.pack "destination"+		ultimateDest <- case dest of+			Just (Aeson.String s) -> return s+			_ -> o' .: T.pack "user"+		ResolvedAlias <$> (+				Alias ultimateDest <$>+				(o' .: T.pack "domain")+			)                                    <*>+			(o' .: T.pack "destination_address" >>= readZ) <*>+			(o' .:? T.pack "dt")+	parseJSON _ = fail "Ripple federation records are always objects."++-- * Resolve aliases++-- | Resolve an alias+resolve :: Alias -> IO (Either Error ResolvedAlias)+resolve a@(Alias u domain) | domain == T.pack "ripple.com" = runEitherT $ do+	FederationResult r <- EitherT $ runUnexceptionalIO $ runEitherT $+		get (URI "https:" (Just $ URIAuth "" "id.ripple.com" "") ("/v1/user/" ++ T.unpack u) "" "") a+	RippleNameResponse x <- hoistEither r+	return x+resolve a@(Alias _ domain) = runEitherT $ do+	txt <- EitherT (getRippleTxt domain)+	uri <- case lookup (T.pack "federation_url") txt of+		Just [url] -> hoistEither $ note+			(Error NoSupported (T.pack "federation_url in ripple.txt is invalid"))+			(parseAbsoluteURI $ T.unpack url)+		_ ->+			throwT $ Error NoSupported (T.pack "No federation_url in ripple.txt")++	FederationResult r <- EitherT $ runUnexceptionalIO $ runEitherT $ get uri a+	hoistEither r++-- | Lookup the ripple.txt for a domain+getRippleTxt ::+	Text -- ^ Domain to lookup+	-> IO (Either Error [(Text, [Text])])+getRippleTxt domain = runUnexceptionalIO $ runEitherT $+	tryOne (uri domain')             <|>+	tryOne (uri ("www." ++ domain')) <|>+	tryOne (uri ("ripple." ++ domain'))+	where+	domain' = T.unpack domain+	uri d = URI "https:" (Just $ URIAuth "" d "") "/ripple.txt" "" ""+	tryOne uri =+		(hoistEither =<<) $+		fmapLT (const $ Error Unavailable (T.pack "Network error")) $ fromIO $+		oneShotHTTP HttpStreams.GET uri+		(setContentLength 0 >> setAccept (BS8.pack "text/plain"))+		HttpStreams.emptyBody (parseResponse rippleTxtParser)++-- | Attoparsec parser for ripple.txt+rippleTxtParser :: Attoparsec.Parser [(Text, [Text])]+rippleTxtParser = some section+	where+	section = do+		h <- header >>= utf8+		ls <- many (Attoparsec.eitherP comment line)+		ls' <- mapM utf8 (rights ls)+		return (h, ls')+	utf8 bs = case T.decodeUtf8' bs of+		Right r -> return r+		Left e -> fail $ show e+	header = Attoparsec.skipSpace *>+		Attoparsec.char '[' *> Attoparsec.takeTill(==0x5D) <* Attoparsec.char ']'+	line = Attoparsec.skipSpace *> do+		c <- Attoparsec.peekChar+		guard (c /= Just '[')+		Attoparsec.takeTill Attoparsec.isEndOfLine <* Attoparsec.endOfLine+	comment = Attoparsec.skipSpace *> Attoparsec.char '#' *>+		Attoparsec.takeTill Attoparsec.isEndOfLine <* Attoparsec.endOfLine++-- * Internal Helpers++get :: (QueryLike a, FromJSON b) => URI -> a -> EitherT Error UnexceptionalIO b+get uri payload =+	(hoistEither =<<) $+	fmapLT (const $ Error Unavailable (T.pack "Network error")) $ fromIO $+	oneShotHTTP HttpStreams.GET uri'+	(setContentLength 0 >> setAccept (BS8.pack "application/json"))+	HttpStreams.emptyBody safeJSONresponse+	where+	uri' = uri { uriQuery = BS8.unpack $ renderQuery True (toQuery payload)}++safeJSONresponse :: (Aeson.FromJSON a) => Response -> InputStream ByteString -> IO (Either Error a)+safeJSONresponse resp i = runEitherT $ do+	v <- EitherT $ parseResponse Aeson.json' resp i+	case Aeson.fromJSON v of+		Aeson.Success a -> return a+		Aeson.Error e -> throwT $ Error Unavailable $+			T.pack $ "JSON parser error: " ++ e++parseResponse :: Attoparsec.Parser a -> Response -> InputStream ByteString -> IO (Either Error a)+parseResponse parser _ i = runUnexceptionalIO $ runEitherT $+	fmapLT (\e -> handle e (fromException e)) $ fromIO $+		parseFromStream parser i+	where+	parseError e = Error Unavailable (T.pack $ "Parse error: " ++ show e)+	handle _ (Just (ParseException e)) = parseError e+	handle e _ = Error Unavailable (T.pack $ "Exception: " ++ show e)++oneShotHTTP :: HttpStreams.Method -> URI -> RequestBuilder () -> (OutputStream Builder -> IO ()) -> (Response -> InputStream ByteString -> IO b) -> IO b+oneShotHTTP method uri req body handler = do+	req' <- buildRequest $ do+		http method (BS8.pack $ uriPath uri ++ uriQuery uri)+		req+	withConnection (establishConnection url) $ \conn -> do+		sendRequest conn req' body+		receiveResponse conn handler+	where+	url = BS8.pack $ show uri -- URI can only have ASCII, so should be safe++-- * Temporary stuff to handle centralised Ripple Names hack++newtype RippleNameResponse = RippleNameResponse ResolvedAlias++instance FromJSON RippleNameResponse where+	parseJSON (Aeson.Object o) = RippleNameResponse <$> (+		ResolvedAlias <$> (+				Alias                   <$>+				(o .: T.pack "username") <*>+				return (T.pack "ripple.com")+			)                                    <*>+			(o .: T.pack "address" >>= readZ) <*>+			return Nothing+		)+	parseJSON _ = fail "Ripple federation records are always objects."
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple++main = defaultMain
+ ripple-federation.cabal view
@@ -0,0 +1,43 @@+name:            ripple-federation+version:         0.1+cabal-version:   >= 1.8+license:         OtherLicense+license-file:    COPYING+category:        Data+copyright:       © 2014 Stephen Paul Weber+author:          Stephen Paul Weber <singpolyma@singpolyma.net>+maintainer:      Stephen Paul Weber <singpolyma@singpolyma.net>+stability:       experimental+tested-with:     GHC == 7.4.1+synopsis:        Utilities and types to work with the Ripple federation protocol+homepage:        https://github.com/singpolyma/ripple-federation-haskell+bug-reports:     https://github.com/singpolyma/ripple-federation-haskell/issues+build-type:      Simple+description:+        To resolve federation names, etc.++extra-source-files:+        README++library+        exposed-modules:+                Ripple.Federation++        build-depends:+                base == 4.*,+                bytestring,+                text,+                errors,+                aeson,+                attoparsec,+                blaze-builder,+                network,+                http-types,+                http-streams,+                io-streams,+                unexceptionalio,+                base58address++source-repository head+        type:     git+        location: git://github.com/singpolyma/ripple-federation-haskell.git