@ -83,6 +83,8 @@ var (
noauthFlag = flag . Bool ( "noauth" , false , "Enables funding requests without authentication" )
logFlag = flag . Int ( "loglevel" , 3 , "Log level to use for Ethereum and the faucet" )
twitterBearerToken = flag . String ( "twitter.token" , "" , "Twitter bearer token to authenticate with the twitter API" )
)
var (
@ -443,6 +445,7 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
}
// Retrieve the Ethereum address to fund, the requesting user and a profile picture
var (
id string
username string
avatar string
address common . Address
@ -462,11 +465,13 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
}
continue
case strings . HasPrefix ( msg . URL , "https://twitter.com/" ) :
username , avatar , address , err = authTwitter ( msg . URL )
id , username , avatar , address , err = authTwitter ( msg . URL , * twitterBearerToken )
case strings . HasPrefix ( msg . URL , "https://www.facebook.com/" ) :
username , avatar , address , err = authFacebook ( msg . URL )
id = username
case * noauthFlag :
username , avatar , address , err = authNoAuth ( msg . URL )
id = username
default :
//lint:ignore ST1005 This error is to be displayed in the browser
err = errors . New ( "Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues" )
@ -486,7 +491,7 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
fund bool
timeout time . Time
)
if timeout = f . timeouts [ username ] ; time . Now ( ) . After ( timeout ) {
if timeout = f . timeouts [ id ] ; time . Now ( ) . After ( timeout ) {
// User wasn't funded recently, create the funding transaction
amount := new ( big . Int ) . Mul ( big . NewInt ( int64 ( * payoutFlag ) ) , ether )
amount = new ( big . Int ) . Mul ( amount , new ( big . Int ) . Exp ( big . NewInt ( 5 ) , big . NewInt ( int64 ( msg . Tier ) ) , nil ) )
@ -520,7 +525,7 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) {
timeout := time . Duration ( * minutesFlag * int ( math . Pow ( 3 , float64 ( msg . Tier ) ) ) ) * time . Minute
grace := timeout / 288 // 24h timeout => 5m grace
f . timeouts [ username ] = time . Now ( ) . Add ( timeout - grace )
f . timeouts [ id ] = time . Now ( ) . Add ( timeout - grace )
fund = true
}
f . lock . Unlock ( )
@ -684,23 +689,32 @@ func sendSuccess(conn *websocket.Conn, msg string) error {
}
// authTwitter tries to authenticate a faucet request using Twitter posts, returning
// the username, avatar URL and Ethereum address to fund on success.
func authTwitter ( url string ) ( string , string , common . Address , error ) {
// the uniqueness identifier (user id/username), u sername, avatar URL and Ethereum address to fund on success.
func authTwitter ( url string , token string ) ( string , string , string , common . Address , error ) {
// Ensure the user specified a meaningful URL, no fancy nonsense
parts := strings . Split ( url , "/" )
if len ( parts ) < 4 || parts [ len ( parts ) - 2 ] != "status" {
//lint:ignore ST1005 This error is to be displayed in the browser
return "" , "" , common . Address { } , errors . New ( "Invalid Twitter status URL" )
return "" , "" , "" , common . Address { } , errors . New ( "Invalid Twitter status URL" )
}
// Twitter's API isn't really friendly with direct links.
// It is restricted to 300 queries / 15 minute with an app api key.
// Anything more will require read only authorization from the users and that we want to avoid.
// If twitter bearer token is provided, use the twitter api
if token != "" {
return authTwitterWithToken ( parts [ len ( parts ) - 1 ] , token )
}
// Twitter's API isn't really friendly with direct links. Still, we don't
// want to do ask read permissions from users, so just load the public posts
// Twiter API token isn't provided so we just load the public posts
// and scrape it for the Ethereum address and profile URL. We need to load
// the mobile page though since the main page loads tweet contents via JS.
url = strings . Replace ( url , "https://twitter.com/" , "https://mobile.twitter.com/" , 1 )
res , err := http . Get ( url )
if err != nil {
return "" , "" , common . Address { } , err
return "" , "" , "" , common . Address { } , err
}
defer res . Body . Close ( )
@ -708,24 +722,77 @@ func authTwitter(url string) (string, string, common.Address, error) {
parts = strings . Split ( res . Request . URL . String ( ) , "/" )
if len ( parts ) < 4 || parts [ len ( parts ) - 2 ] != "status" {
//lint:ignore ST1005 This error is to be displayed in the browser
return "" , "" , common . Address { } , errors . New ( "Invalid Twitter status URL" )
return "" , "" , "" , common . Address { } , errors . New ( "Invalid Twitter status URL" )
}
username := parts [ len ( parts ) - 3 ]
body , err := ioutil . ReadAll ( res . Body )
if err != nil {
return "" , "" , common . Address { } , err
return "" , "" , "" , common . Address { } , err
}
address := common . HexToAddress ( string ( regexp . MustCompile ( "0x[0-9a-fA-F]{40}" ) . Find ( body ) ) )
if address == ( common . Address { } ) {
//lint:ignore ST1005 This error is to be displayed in the browser
return "" , "" , common . Address { } , errors . New ( "No Ethereum address found to fund" )
return "" , "" , "" , common . Address { } , errors . New ( "No Ethereum address found to fund" )
}
var avatar string
if parts = regexp . MustCompile ( "src=\"([^\"]+twimg.com/profile_images[^\"]+)\"" ) . FindStringSubmatch ( string ( body ) ) ; len ( parts ) == 2 {
avatar = parts [ 1 ]
}
return username + "@twitter" , avatar , address , nil
return username + "@twitter" , username , avatar , address , nil
}
// authTwitterWithToken tries to authenticate a faucet request using Twitter's API, returning
// the uniqueness identifier (user id/username), username, avatar URL and Ethereum address to fund on success.
func authTwitterWithToken ( tweetID string , token string ) ( string , string , string , common . Address , error ) {
// Strip any query parameters from the tweet id
sanitizedTweetID := strings . Split ( tweetID , "?" ) [ 0 ]
// Ensure numeric tweetID
if ! regexp . MustCompile ( "^[0-9]+$" ) . MatchString ( sanitizedTweetID ) {
return "" , "" , "" , common . Address { } , errors . New ( "Invalid Tweet URL" )
}
// Query the tweet details from Twitter
url := fmt . Sprintf ( "https://api.twitter.com/2/tweets/%s?expansions=author_id&user.fields=profile_image_url" , sanitizedTweetID )
req , err := http . NewRequest ( "GET" , url , nil )
if err != nil {
return "" , "" , "" , common . Address { } , err
}
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , token ) )
res , err := http . DefaultClient . Do ( req )
if err != nil {
return "" , "" , "" , common . Address { } , err
}
defer res . Body . Close ( )
var result struct {
Data struct {
AuthorID string ` json:"author_id" `
ID string ` json:"id" `
Text string ` json:"text" `
} ` json:"data" `
Includes struct {
Users [ ] struct {
ProfileImageURL string ` json:"profile_image_url" `
Username string ` json:"username" `
ID string ` json:"id" `
Name string ` json:"name" `
} ` json:"users" `
} ` json:"includes" `
}
err = json . NewDecoder ( res . Body ) . Decode ( & result )
if err != nil {
return "" , "" , "" , common . Address { } , err
}
address := common . HexToAddress ( regexp . MustCompile ( "0x[0-9a-fA-F]{40}" ) . FindString ( result . Data . Text ) )
if address == ( common . Address { } ) {
//lint:ignore ST1005 This error is to be displayed in the browser
return "" , "" , "" , common . Address { } , errors . New ( "No Ethereum address found to fund" )
}
return result . Data . AuthorID + "@twitter" , result . Includes . Users [ 0 ] . Username , result . Includes . Users [ 0 ] . ProfileImageURL , address , nil
}
// authFacebook tries to authenticate a faucet request using Facebook posts,