@@ -28,18 +28,28 @@ import Data.Map (Map)
28
28
import qualified Data.Map as Map
29
29
import Data.Text (Text )
30
30
import qualified Data.Text as T
31
+ import qualified Data.Text.Encoding as T
31
32
import qualified Data.ByteString.Char8 as BS -- Only used for ASCII data
32
-
33
+ import qualified Data.ByteString.Lazy as BSL
33
34
import Data.Typeable (Typeable )
34
35
import Control.Monad.Reader (ask )
35
36
import Control.Monad.State (get , put , modify )
36
37
import Data.SafeCopy (base , deriveSafeCopy )
37
38
38
39
import Distribution.Text (display )
39
- import Data.Time ( UTCTime ( .. ), getCurrentTime , addDays )
40
+ import Data.Time
40
41
import Text.CSV (CSV , Record )
41
42
import Network.Mail.Mime
42
43
import 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
43
53
44
54
45
55
-- | A feature to allow open account signup, and password reset,
@@ -306,6 +316,7 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
306
316
userSignupFeatureInterface = (emptyHackageFeature " user-signup-reset" ) {
307
317
featureDesc = " Extra information about user accounts, email addresses etc."
308
318
, featureResources = [signupRequestsResource,
319
+ captchaResource,
309
320
signupRequestResource,
310
321
resetRequestsResource,
311
322
resetRequestResource]
@@ -325,6 +336,12 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
325
336
, resourceGet = [ (" " , handlerGetSignupRequestNew) ]
326
337
, resourcePost = [ (" " , handlerPostSignupRequestNew) ]
327
338
}
339
+
340
+ captchaResource =
341
+ (resourceAt " /users/register/captcha" ) {
342
+ resourceDesc = [ (GET , " Get a new captcha" ) ]
343
+ , resourceGet = [ (" json" , handlerGetCaptcha) ]
344
+ }
328
345
329
346
signupRequestResource =
330
347
(resourceAt " /users/register-request/:nonce" ) {
@@ -413,20 +430,44 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
413
430
[MText $ " The " ++ thing ++ " token does not exist. It could be that it "
414
431
++ " has been used already, or that it has expired." ]
415
432
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
+
416
442
handlerGetSignupRequestNew :: DynamicPath -> ServerPartE Response
417
443
handlerGetSignupRequestNew _ = do
444
+ (timestamp, hash, base64image) <- liftIO makeCaptchaHash
418
445
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
+ ]
420
460
421
461
handlerPostSignupRequestNew :: DynamicPath -> ServerPartE Response
422
462
handlerPostSignupRequestNew _ = do
423
463
templateEmail <- getTemplate templates " SignupConfirmation.email"
424
464
templateConfirmation <- getTemplate templates " SignupEmailSent.html"
425
465
426
- (username, realname, useremail) <- lookUserNameEmail
466
+ timestamp <- liftIO getCurrentTime
467
+
468
+ (username, realname, useremail) <- lookValidFields timestamp
427
469
428
470
nonce <- liftIO (newRandomNonce 10 )
429
- timestamp <- liftIO getCurrentTime
430
471
let signupInfo = SignupInfo {
431
472
signupUserName = username,
432
473
signupRealName = realname,
@@ -462,17 +503,29 @@ userSignupFeature ServerEnv{serverBaseURI, serverCron}
462
503
templateConfirmation
463
504
[ " useremail" $= useremail ]
464
505
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"
470
514
, errBadRequest " Missing form fields" [] ]
471
515
472
516
guardValidLookingUserName username
473
517
guardValidLookingName realname
474
518
guardValidLookingEmail useremail
475
519
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
+
476
529
return (username, realname, useremail)
477
530
478
531
handlerGetSignupRequestOutstanding :: DynamicPath -> ServerPartE Response
0 commit comments