@@ -28,18 +28,28 @@ import Data.Map (Map)
2828import qualified Data.Map as Map
2929import Data.Text (Text )
3030import qualified Data.Text as T
31+ import qualified Data.Text.Encoding as T
3132import qualified Data.ByteString.Char8 as BS -- Only used for ASCII data
32-
33+ import qualified Data.ByteString.Lazy as BSL
3334import Data.Typeable (Typeable )
3435import Control.Monad.Reader (ask )
3536import Control.Monad.State (get , put , modify )
3637import Data.SafeCopy (base , deriveSafeCopy )
3738
3839import Distribution.Text (display )
39- import Data.Time ( UTCTime ( .. ), getCurrentTime , addDays )
40+ import Data.Time
4041import Text.CSV (CSV , Record )
4142import Network.Mail.Mime
4243import Network.URI (URI (.. ), URIAuth (.. ))
44+ import Graphics.Captcha
45+ import qualified Data.ByteString.Base64 as Base64
46+ import qualified Crypto.Hash.SHA256 as SHA256
47+ import Data.String
48+ import Data.Char
49+ import Text.Read (readMaybe )
50+ import Data.Aeson
51+ import qualified Data.Aeson.KeyMap as KeyMap
52+ import qualified Data.Aeson.Key as Key
4353
4454
4555-- | A feature to allow open account signup, and password reset,
@@ -306,6 +316,7 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
306316 userSignupFeatureInterface = (emptyHackageFeature " user-signup-reset" ) {
307317 featureDesc = " Extra information about user accounts, email addresses etc."
308318 , featureResources = [signupRequestsResource,
319+ captchaResource,
309320 signupRequestResource,
310321 resetRequestsResource,
311322 resetRequestResource]
@@ -325,6 +336,12 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
325336 , resourceGet = [ (" " , handlerGetSignupRequestNew) ]
326337 , resourcePost = [ (" " , handlerPostSignupRequestNew) ]
327338 }
339+
340+ captchaResource =
341+ (resourceAt " /users/register/captcha" ) {
342+ resourceDesc = [ (GET , " Get a new captcha" ) ]
343+ , resourceGet = [ (" json" , handlerGetCaptcha) ]
344+ }
328345
329346 signupRequestResource =
330347 (resourceAt " /users/register-request/:nonce" ) {
@@ -413,20 +430,44 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
413430 [MText $ " The " ++ thing ++ " token does not exist. It could be that it "
414431 ++ " has been used already, or that it has expired." ]
415432
433+ hashTimeAndCaptcha :: UTCTime -> String -> BS. ByteString
434+ hashTimeAndCaptcha timestamp captcha = Base64. encode (SHA256. hash (fromString (show timestamp ++ map toUpper captcha)))
435+
436+ makeCaptchaHash :: IO (UTCTime , BS. ByteString , BS. ByteString )
437+ makeCaptchaHash = do
438+ (code, image) <- makeCaptcha
439+ timestamp <- getCurrentTime
440+ pure (timestamp, hashTimeAndCaptcha timestamp code, fromString " data:image/png;base64," <> Base64. encode image)
441+
416442 handlerGetSignupRequestNew :: DynamicPath -> ServerPartE Response
417443 handlerGetSignupRequestNew _ = do
444+ (timestamp, hash, base64image) <- liftIO makeCaptchaHash
418445 template <- getTemplate templates " SignupRequest.html"
419- ok $ toResponse $ template []
446+ ok $ toResponse $ template
447+ [ " timestamp" $= timestamp
448+ , " hash" $= hash
449+ , " base64image" $= base64image
450+ ]
451+
452+ handlerGetCaptcha :: DynamicPath -> ServerPartE Response
453+ handlerGetCaptcha _ = do
454+ (timestamp, hash, base64image) <- liftIO makeCaptchaHash
455+ ok $ toResponse $ Object $ KeyMap. fromList $
456+ [ (Key. fromString " timestamp" , String (T. pack (show timestamp)))
457+ , (Key. fromString " hash" , String (T. decodeUtf8 hash))
458+ , (Key. fromString " base64image" , String (T. decodeUtf8 base64image))
459+ ]
420460
421461 handlerPostSignupRequestNew :: DynamicPath -> ServerPartE Response
422462 handlerPostSignupRequestNew _ = do
423463 templateEmail <- getTemplate templates " SignupConfirmation.email"
424464 templateConfirmation <- getTemplate templates " SignupEmailSent.html"
425465
426- (username, realname, useremail) <- lookUserNameEmail
466+ timestamp <- liftIO getCurrentTime
467+
468+ (username, realname, useremail) <- lookValidFields timestamp
427469
428470 nonce <- liftIO (newRandomNonce 10 )
429- timestamp <- liftIO getCurrentTime
430471 let signupInfo = SignupInfo {
431472 signupUserName = username,
432473 signupRealName = realname,
@@ -462,17 +503,29 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
462503 templateConfirmation
463504 [ " useremail" $= useremail ]
464505 where
465- lookUserNameEmail = do
466- (username, realname, useremail) <-
467- msum [ body $ (,,) <$> lookText' " username"
468- <*> lookText' " realname"
469- <*> lookText' " email"
506+ lookValidFields now = do
507+ (username, realname, useremail, captcha, timestampStr, hash) <-
508+ msum [ body $ (,,,,,) <$> lookText' " username"
509+ <*> lookText' " realname"
510+ <*> lookText' " email"
511+ <*> look " captcha"
512+ <*> look " timestamp"
513+ <*> lookBS " hash"
470514 , errBadRequest " Missing form fields" [] ]
471515
472516 guardValidLookingUserName username
473517 guardValidLookingName realname
474518 guardValidLookingEmail useremail
475519
520+ timestamp <- maybe (errBadRequest " Invalid request" [MText " Seems something went wrong with your request." ])
521+ pure (readMaybe timestampStr)
522+
523+ when (diffUTCTime now timestamp > secondsToNominalDiffTime (10 * 60 )) $
524+ errBadRequest " Problem with captcha" [MText " Oops, The captcha has expired. Please be quick next time!" ]
525+
526+ unless (hashTimeAndCaptcha timestamp captcha == BSL. toStrict hash) $
527+ errBadRequest " Problem with captcha" [MText " Sorry, the captcha is wrong. Please try sign up again." ]
528+
476529 return (username, realname, useremail)
477530
478531 handlerGetSignupRequestOutstanding :: DynamicPath -> ServerPartE Response
0 commit comments