diff --git a/COPYING b/COPYING
new file mode 100644
--- /dev/null
+++ b/COPYING
@@ -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.
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,1 @@
+To resolve federation names, etc.
diff --git a/Ripple/Federation.hs b/Ripple/Federation.hs
new file mode 100644
--- /dev/null
+++ b/Ripple/Federation.hs
@@ -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."
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,3 @@
+import Distribution.Simple
+
+main = defaultMain
diff --git a/ripple-federation.cabal b/ripple-federation.cabal
new file mode 100644
--- /dev/null
+++ b/ripple-federation.cabal
@@ -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
