packages feed

hellnet-0.1: Hellnet/Network.hs

--------------------------------------------------------------------------------
--     This file is part of Hellnet
--
--     Hellnet is free software: you can redistribute it and/or modify
--     it under the terms of the GNU General Public License as published by
--     the Free Software Foundation, either version 3 of the License, or
--     (at your option) any later version.
--
--     Hellnet is distributed in the hope that it will be useful,
--     but WITHOUT ANY WARRANTY; without even the implied warranty of
--     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--     GNU General Public License for more details.
--
--     You should have received a copy of the GNU General Public License
--     along with Hellnet.  If not, see <http://www.gnu.org/licenses/>.
--------------------------------------------------------------------------------

module Hellnet.Network (
	addNode
	, fetchChunk
	, fetchChunks
	, fetchFile
	, fetchMeta
	, fetchMetaFromNode
	, fetchNodeListFromNode
	, findChunk
	, findChunks
	, findFile
	, findKey
	, findMetaContent
	, findMetaContent'
	, findMetaContentByName
	, findMetaContentByName'
	, findMetaValue
	, findURI
	, getContactLog
	, getNodesList
	, handshakeWithNode
	, queryNodeGet
	, updateNodeContactTime
	, verifyMeta
	, writeContactLog
	, writeNodesList
	) where

import Codec.Crypto.RSA as RSA
import Codec.Utils
import Control.Concurrent
import Control.Monad
import qualified Control.Exception as Ex
import qualified Data.ByteString.UTF8 as BU
import qualified Data.ByteString.Lazy.UTF8 as BUL
import Data.List
import Data.Maybe
import Data.Map (Map(..))
import qualified Data.Map as Map
import Debug.Trace
import Hellnet
import Hellnet.Crypto
import Hellnet.Meta as Meta
import Hellnet.Storage
import Hellnet.URI
import Hellnet.Utils
import Network.HTTP
import Network.HTTP.Base
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSL8
import qualified Data.ByteString.Lazy.UTF8 as BUL
import Random
import Safe
import System.IO.Error
import Text.HJson as JSON
import Text.JSON.JPath

getNodesList :: IO [Node]
getNodesList = do
	listfile <- getFile =<< toFullPath "nodelist"
	let nodes = maybe (Nothing) (readMay . BSL8.unpack) listfile
	return $ maybe [] (id) nodes

writeNodesList :: [Node] -> IO ()
writeNodesList ns = do
	storeFile "nodelist" $ BSL8.pack $ show ns

-- | adds node to list if it isn't already there, returns true
-- | otherwise returns false
addNode :: Node -> IO Bool
addNode node = do
	nL <- getNodesList
	if node `elem` nL then return False
		else do
			writeNodesList (node:nL)
			return True

-- | retrieves pieces using servers node list and returns list of pieces that are unavailable.
fetchChunks :: [Hash] -> IO [Hash]
fetchChunks cs = do
	nodes <- getNodesList
	decoys <- mapM (const genHash) [0..(((length cs) `div` 3) + 1)]
	let decoys' = decoys \\ cs
	chunks <- shuffle (cs ++ decoys')
	let todos = splitFor numThreads chunks
	workers <- mapM (forkChild) $ map (fetchChunksFromNodes nodes) todos
	nps <- mapM (takeMVar) workers
	return ((concat nps) \\ decoys')

-- | for usage in workers
fetchChunksFromNodes :: [Node] -> [Hash] -> IO [Hash]
fetchChunksFromNodes ns cs = do
	nodes <- shuffle ns
	let fs = map (fetchChunksFromNode) ns
	return =<< filtM fs cs

fetchChunksFromNode :: Node -> [Hash] -> IO [Hash]
fetchChunksFromNode s cs = filterM (fetchChunk' s) cs

fetchChunk' :: Node -> Hash -> IO Bool
fetchChunk' s p = do
	b <- fetchChunk s p
	return (not b)

-- | retrieves chunk using server, returns success status
fetchChunk :: Node -> Hash -> IO Bool
fetchChunk node p = do
	let chunkID = hashToHex p
	let reqString = "chunks/" ++ (take 2 chunkID) ++ "/" ++ (drop 2 chunkID)
	req <- queryNodeGet reqString node
	maybe (return False)
		(\r -> do
			chID <- insertChunk Nothing r
			if (chID == p) then
				return True
				else do
					purgeChunk chID
					return False
			) req

-- | find file, returns either not found chunks or file in lazy ByteString
findFile :: Maybe Key -> Hash -> IO (Either [Hash] BSL.ByteString)
findFile key hsh = do
	link <- findChunk key hsh
	maybe (return (Left [hsh])) (\l -> do
		res <- findFile' key l
		return res
		) link

findFile' :: Maybe Key -> Chunk -> IO (Either [Hash] BSL.ByteString)
findFile' key cs = do
	let chs = splitBslFor hashSize cs
	chs' <- findChunks key $ map (BSL.unpack) chs
	either (return . Left)
		(\c -> if (length chs) == (hashesPerChunk + 1) then do
			f <- findFile' key (last c)
			either (return . Left) (\ff -> return $ Right $ BSL.concat [BSL.concat $ init c, ff]) f
			else
			return $ Right $ BSL.concat c
		) chs'

-- | tries to locate chunks, returns either list of unavailable ones or list of chunks' content
findChunks :: Maybe Key -> [Hash] -> IO (Either [Hash] [Chunk])
findChunks key chs = do
	res <- mapM (getChunk key) chs
	let unavailable = map (fst) (filter ((== Nothing) . snd) (zip chs res))
	if null unavailable then
		return (Right (catMaybes res))
		else do
			fromNet <- fetchChunks unavailable
			if null fromNet then do
				chs' <- mapM (getChunk key) chs
				return (Right (catMaybes chs'))
				else
				return (Left fromNet)

findChunk :: Maybe Key -> Hash -> IO (Maybe Chunk)
findChunk key hsh = do
	fC <- findChunks key [hsh]
	either (const (return Nothing)) (return . Just . head) fC

-- | prepares file for collecting from storage, returns either list of unavailable chunks or unrolled filelink
fetchFile :: Maybe Key -> Hash -> IO (Either [Hash] [Hash])
fetchFile encKey hsh = do
	link <- findChunk encKey hsh
	maybe (return (Left [hsh])) (\l -> do
		res <- fetchFile' encKey l
		return res
		) link

fetchFile' :: Maybe Key -> Chunk -> IO (Either [Hash] [Hash])
fetchFile' encKey cs = do
	let chs = splitFor hashSize $ BSL.unpack cs
	stored <- filterM (isStored) chs
	chs' <- if stored /= chs then
		fetchChunks chs
		else
		return []
	if	not (null chs') then do
		return (Left chs')
		else
		if (length chs) == (hashesPerChunk + 1) then do
			c <- getChunk encKey $ last chs
			f <- fetchFile' encKey $ unjust c
			return (either (Left) (Right . ((++) (init chs))) f)
			else
			return (Right chs)

queryNodeGet :: String -> Node -> IO (Maybe BSL.ByteString)
queryNodeGet s node = do
	rep <- ((return . Just) =<< simpleHTTP (mkRequest GET $ mkUrl node s)) `catch` const (return Nothing)
	return $ maybe Nothing (either (const Nothing) (\rsp -> if rspCode rsp == (2,0,0) then Just (rspBody rsp) else Nothing)) rep

queryNodePost :: String -> [(String, String)] -> Node -> IO (Maybe BSL.ByteString)
queryNodePost s fields node = do
	let dat = BSL8.pack $ urlEncodeVars fields
	let url = mkUrl node s
	let request = (mkRequest POST url :: Request BSL.ByteString) {rqBody=dat, rqHeaders=[Header HdrContentType "application/x-www-form-urlencoded",
		Header HdrContentLength $ show $ BSL8.length dat]}
	rep <- ((return . Just) =<< simpleHTTP request) `catch` const (return Nothing)
	return $ maybe Nothing (either (const Nothing) (\rsp -> if rspCode rsp == (2,0,0) then Just (rspBody rsp) else Nothing)) rep

-- | Perform handshake with node. Return Left (error message) if connection failed or Right (handshake success)
handshakeWithNode :: Node -> IO (Either String Bool)
handshakeWithNode node = do
	sP <- getFile "serverport"
	result <- maybe (return $ Left "Server not running?") (\serverPort -> do
		res <- queryNodePost "handshake" [("port", BSL8.unpack serverPort)] node
		return $ maybe (Left "Failed to contact node") (\r -> Right $ (BSL8.unpack r) `elem` ["OK","EXISTS"]) res
		) sP
	return result

updateNodeContactTime :: String -> Integer -> IO ()
updateNodeContactTime hst tim = do
	tehLog <- getContactLog
	writeContactLog (Map.insert hst tim tehLog)

getContactLog :: IO (Map String Integer)
getContactLog = do
	fil <- getFile "contactlog"
	let res = maybe (Nothing) (readMay . BSL8.unpack) fil
	return $ maybe (Map.fromList []) (id) res

writeContactLog :: (Map String Integer) -> IO ()
writeContactLog l = storeFile "contactlog" (BSL8.pack $ show l)

-- | Verify meta's signature
-- | Returns Nothing if public key was not found or Just (check result)
verifyMeta :: Meta -> IO (Maybe Bool)
verifyMeta meta
	| isNothing (signature meta) = return $ Just False
	| isNothing (message meta) = return Nothing
	| otherwise = do
		let (Just msg, Just sig) = (message meta, signature meta)
		keyChunk <- findChunk Nothing (keyID meta)
		maybe (return Nothing) (\ch -> do
			let str = BUL.toString ch
			either (const (return Nothing)) (\j -> do
				maybe (return Nothing) (\k -> do
					return $ Just $ verifyAsym k msg sig
					) $ fromJson j
				) $ JSON.fromString str
			) keyChunk

fetchMeta :: KeyID -> String -> IO Bool
fetchMeta keyId mName = do
	nodes <- getNodesList >>= shuffle
	currentVersion <- getMeta keyId mName
	fetched <- fetchMetaFromNodes nodes currentVersion keyId mName
	case fetched of
		Nothing -> return False
		Just m -> do
			verified <- verifyMeta m
			case verified of
				Just True -> do
					storeMeta m
					return True
				otherwise -> return False

fetchMetaFromNodes :: [Node] -- ^ Nodes' list
	-> Maybe Meta -- ^ Current (latest) version of this meta
	-> KeyID -- ^ Meta's public key ID
	-> String -- ^ Meta name
	-> IO (Maybe Meta)
fetchMetaFromNodes (n:ns) currentVersion k s = do
	fetched <- fetchMetaFromNode n k s
	case fetched of
		Just a -> case currentVersion of
			Nothing -> fetchMetaFromNodes ns fetched k s
			Just b -> let newerVersion = if timestamp a > timestamp b then a else b
				in fetchMetaFromNodes ns (Just newerVersion) k s
		Nothing -> fetchMetaFromNodes ns currentVersion k s
fetchMetaFromNodes [] cv _ _ = return cv

fetchMetaFromNode :: Node -> KeyID -> String -> IO (Maybe Meta)
fetchMetaFromNode node keyId mName = do
	result <- queryNodeGet (intercalate "/" ["meta", hashToHex keyId, mName]) node
	maybe (return Nothing) (\bs -> do
		let meta = Meta.fromByteString bs
		return meta
		) result

findMetaContent :: Meta -> IO (Maybe Json)
findMetaContent m = do
	cont <- findMetaContent' m
	return $ case cont of
		Nothing -> Nothing
		Just s -> case JSON.fromString s of
			Left _ -> Nothing
			Right js -> Just js

findMetaContent' :: Meta -> IO (Maybe String)
findMetaContent' m = do
	cont <- findURI (contentURI m)
	return $ case cont of
		Nothing -> Nothing
		Just bs -> case JSON.fromString (BUL.toString bs) of
			Left _ -> Nothing
			Right js -> Just $ BUL.toString bs

findMetaContentByName :: KeyID -> String -> IO (Maybe Json)
findMetaContentByName kid mname = do
	metaM <- getMeta kid mname
	case metaM of
		Nothing -> return Nothing
		Just meta -> findMetaContent meta

findMetaContentByName' :: KeyID -> String -> IO (Maybe String)
findMetaContentByName' kid mname = do
	metaM <- getMeta kid mname
	case metaM of
		Nothing -> return Nothing
		Just meta -> findMetaContent' meta

findMetaValue :: KeyID -- ^ public key ID
	-> String -- ^ Meta name
	-> String -- ^ Meta JPath
	-> IO (Maybe [Json]) -- ^ Results or Nothing if meta was not found
findMetaValue keyId mName mPath = do
	meta <- getMeta keyId mName
	case meta of
		Nothing -> return Nothing
		Just m -> do
			cont <- findMetaContent m
			case cont of
				Nothing -> return Nothing
				Just j -> return $ Just $ jPath' mPath j

fetchNodeListFromNode :: Node -> IO [Node]
fetchNodeListFromNode node = do
	results <- queryNodeGet "/nodelist" node
	return (fromMaybe [] (maybe Nothing (readMay . BSL8.unpack) results))

findURI :: HellnetURI -> IO (Maybe BSL.ByteString)
findURI uri = case uri of
	(ChunkURI hsh key fname) -> do
		findChunk key hsh
	(FileURI hsh key fname) -> do
		fil <- findFile key hsh
		return $ either (const Nothing) (Just) fil
	otherwise -> return Nothing

findKey :: KeyID -> IO (Maybe PublicKey)
findKey kid = do
	ch <- findChunk Nothing kid
	return $ case ch of
		Nothing -> Nothing
		Just chunk -> either (const Nothing) (fromJson) $ JSON.fromString $ BUL.toString $ chunk