@ -11,6 +11,7 @@ import (
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
@ -28,8 +29,30 @@ import (
ua "github.com/mileusna/useragent"
)
var errTokenNotVerified = apiError { "token has not been verified" }
var errUsedToken = apiError { "token has already been used" }
var errInvalidEmail = apiError { "invalid email address" }
// API Errors
type serverError struct {
error string
}
func ( e serverError ) Error ( ) string {
return e . error
}
type apiError struct {
error string
}
func ( e apiError ) Error ( ) string {
return e . error
}
type HTTPResponse struct {
Error string ` json:"error" `
Error string ` json:"error,omitempty" `
Code string ` json:"code,omitempty" `
Success bool ` json:"success" `
}
@ -135,13 +158,22 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
return
}
contact , err = lintEmail ( contact )
if nil != err {
b , _ := json . Marshal ( & HTTPResponse {
Error : err . Error ( ) ,
Code : "E_USER" ,
} )
w . Write ( b )
return
}
_ , ok , err := contactKV . Load ( contact )
if nil != err {
fmt . Fprintf ( os . Stderr , "meta: error loading contact: %s\n" , err . Error ( ) )
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
return
}
if ok {
b , _ := json . Marshal ( & HTTPResponse {
Success : true ,
@ -152,6 +184,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
b , _ := json . Marshal ( & HTTPResponse {
Error : "not found" ,
Code : "E_USER" ,
} )
w . Write ( b )
} )
@ -217,6 +250,13 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
otp , err = consumeOTPReceipt ( "TODO" , receipt , agent , addr )
}
if nil != err {
if errTokenNotVerified == err {
b , _ := json . Marshal ( & HTTPResponse {
Error : err . Error ( ) ,
} )
w . Write ( b )
return
}
// TODO propagate error types
http . Error ( w , "Bad Request" , http . StatusBadRequest )
fmt . Fprintf ( w , "%s" , err )
@ -821,7 +861,7 @@ func newOTP(email string, agent string, addr string) (id string, secret []byte,
// keep it secret, keep it safe
os . FileMode ( 0600 ) ,
) ; nil != err {
return "" , nil , errors . New ( "database connection failed when writing verification token" )
return "" , nil , serverError { "database connection failed when writing verification token" }
}
return receipt , secret , nil
}
@ -847,12 +887,12 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error
tokfile := filepath . Join ( tokenPrefix , hash + ".tok.txt" )
b , err := ioutil . ReadFile ( tokfile )
if nil != err {
return nil , errors . New ( "database connection failed when reading verification token" )
return nil , serverError { "database connection failed when reading verification token" }
}
otp := OTP { }
if err := json . Unmarshal ( b , & otp ) ; nil != err {
return nil , errors . New ( "database verification token parse failed" )
return nil , serverError { "database verification token parse failed" }
}
if 0 == subtle . ConstantTimeCompare ( [ ] byte ( otp . Email ) , [ ] byte ( email ) ) {
@ -862,7 +902,7 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error
if consume . secret {
if ! otp . SecretUsed . IsZero ( ) {
return nil , errors . New ( "token has already been used" )
return nil , errUsedToken
}
otp . SecretUsed = time . Now ( )
if addr != otp . ReceiptIP {
@ -874,10 +914,10 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error
}
} else if consume . receipt {
if otp . SecretUsed . IsZero ( ) {
return nil , errors . New ( "token has not been verified" )
return nil , errTokenNotVerified
}
if ! otp . ReceiptUsed . IsZero ( ) {
return nil , errors . New ( "token has already been used" )
return nil , errUsedToken
}
otp . ReceiptUsed = time . Now ( )
}
@ -890,24 +930,38 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error
// keep it secret, keep it safe
os . FileMode ( 0600 ) ,
) ; nil != err {
return nil , errors . New ( "database connection failed when consuming token" )
return nil , serverError { "database connection failed when consuming token" }
}
}
fmt . Println ( "THE TOKEN IS GOOD. GOOD!!" )
return & otp , nil
}
func lintEmail ( email string ) ( string , error ) {
// TODO check DNS for MX records
parts := strings . Split ( email , "@" )
domain := parts [ 1 ]
if 2 != len ( parts ) || strings . Contains ( email , " \t\n" ) {
return "" , errInvalidEmail
}
mxs , err := net . LookupMX ( domain )
if len ( mxs ) < 1 || nil != err {
// TODO it possible in some cases that this
// could be a network error
return "" , errInvalidEmail
}
return strings . ToLower ( email ) , nil
}
func startVerification ( baseURL , contact , agent , addr string ) ( receipt string , err error ) {
email := strings . Replace ( strings . TrimPrefix ( contact , "mailto:" ) , " " , "+" , - 1 )
if "" == email {
return "" , errors . New ( "missing contact:[\"mailto:me@example.com\"]" )
}
// TODO check DNS for MX records
if ! strings . Contains ( email , "@" ) || strings . Contains ( email , " \t\n" ) {
return "" , errors . New ( "invalid email address" )
email , err = lintEmail ( email )
if nil != err {
return "" , err
}
// TODO expect JWK in JWS/JWT
@ -922,9 +976,10 @@ func startVerification(baseURL, contact, agent, addr string) (receipt string, er
subject := "Verify New Account"
// TODO go tpl
// TODO determine OS and Browser from user agent
page := "pocket/iframe.html"
text := fmt . Sprintf (
"It looks like you just tried to register a new Pocket ID account.\n\n Verify account: %s#/verify/%s\n\n%s on %s %s from %s\n\nNot you? Just ignore this message." ,
baseURL , base64 . RawURLEncoding . EncodeToString ( secret ) , ua . Name , ua . OS , ua . Device , addr ,
"It looks like you just tried to register a new Pocket ID account.\n\n Verify account: %s/%s #/verify/%s\n\n%s on %s %s from %s\n\nNot you? Just ignore this message." ,
baseURL , page , base64 . RawURLEncoding . EncodeToString ( secret ) , ua . Name , ua . OS , ua . Device , addr ,
)
fmt . Println ( "email:" , text )
if ! strings . Contains ( contact , "+noreply" ) {