diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2013, Chris Done
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Chris Done nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/hostname-validate.cabal b/hostname-validate.cabal
new file mode 100644
--- /dev/null
+++ b/hostname-validate.cabal
@@ -0,0 +1,20 @@
+name:                hostname-validate
+version:             1.0.0
+synopsis:            Validate hostnames e.g. localhost or foo.co.uk.
+description:         Validate hostnames e.g. localhost or foo.co.uk. See also RFC 1123, RFC 952, and RFC 1035.
+license:             BSD3
+license-file:        LICENSE
+author:              Chris Done
+maintainer:          chrisdone@gmail.com
+copyright:           2013 Chris Done
+category:            Network
+build-type:          Simple
+cabal-version:       >=1.8
+
+library
+  hs-source-dirs:    src/
+  exposed-modules:   Text.Hostname
+  build-depends:     base >= 4 && < 5,
+                     bytestring,
+                     attoparsec
+  ghc-options:       -O2
diff --git a/src/Text/Hostname.hs b/src/Text/Hostname.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Hostname.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | Validate hostnames.
+
+module Text.Hostname
+  (validHostname)
+  where
+
+import Control.Applicative
+import Data.Attoparsec hiding (Parser)
+import Data.Attoparsec.Combinator
+import Data.Attoparsec.Types (Parser)
+import Data.ByteString (ByteString)
+import GHC.Word
+
+--------------------------------------------------------------------------------
+-- Exported
+
+-- | Is the input a valid host name?
+validHostname :: ByteString -> Bool
+validHostname = test (host >> endOfInput)
+
+--------------------------------------------------------------------------------
+-- Parser
+
+-- | Test the given parser on the given input.
+test :: Parser ByteString b -> ByteString -> Bool
+test p x = either (const False) (const True) (parseOnly p x)
+
+-- | A host name.
+host :: Parser ByteString [[[Word8]]]
+host = labelStart >> many label
+
+-- | A name part.
+name :: Parser ByteString [Word8]
+name = (many1 (char '-') >> many1 diglet) <|> many1 diglet
+
+-- | A host part.
+label :: Parser ByteString [[Word8]]
+label = char '.' >> diglet >> many name
+
+-- | Start of a host part.
+labelStart :: Parser ByteString [[Word8]]
+labelStart = diglet >> many name
+
+-- | Match the character.
+char :: Char -> Parser ByteString Word8
+char c = word8 (fromIntegral (fromEnum c))
+
+-- | ASCII letters and digits.
+diglet :: Parser ByteString Word8
+diglet = satisfy (flip elem (['a'..'z'] ++ ['0'..'9']) . toEnum . fromIntegral)
+
+--------------------------------------------------------------------------------
+-- Unit tests
+
+-- | Do all tests pass?
+testsPass :: Bool
+testsPass = all validHostname correctTests && not (any validHostname incorrectTests)
+
+-- | Tests that should pass.
+correctTests :: [ByteString]
+correctTests =
+  ["a"
+  ,"a.com"
+  ,"a-c"
+  ,"a--b"
+  ,"64"
+  ,"54.com"
+  -- Non-alpha languages use this encoding
+  ,"aaa-bbb-ccc.dooo-bar--zot"
+  ,"xn--mgbh0fb.xn--kgbechtv"
+  ,"xn--fsqu00a.xn--0zwm56d"
+  ,"xn--fsqu00a.xn--g6w251d"
+  ,"xn--hxajbheg2az3al.xn--jxalpdlp"
+  ,"xn--p1b6ci4b4b3a.xn--11b5bs3a9aj6g"
+  ,"xn--r8jz45g.xn--zckzah"
+  ,"xn--9n2bp8q.xn--9t4b11yi5a"
+  ,"xn--mgbh0fb.xn--hgbk6aj7f53bba"
+  ,"xn--e1afmkfd.xn--80akhbyknj4f"
+  ,"xn--zkc6cc5bi7f6e.xn--hlcj6aya9esc7a"
+  ,"xn--6dbbec0c.xn--deba0ad"
+  ,"xn--fdbk5d8ap9b8a8d.xn--deba0ad"]
+
+-- | Tests that should passfail.
+incorrectTests :: [ByteString]
+incorrectTests =
+  [""
+  ,"a-"
+  ,"-"
+  ,"-a"
+  ,"a--"
+  ,"a.-"
+  ,".a"
+  ,".a-z"]
