OSDN Git Service

implement ReCaptcha to work
authorhylom <hylom@users.sourceforge.jp>
Wed, 24 May 2017 13:54:20 +0000 (22:54 +0900)
committerhylom <hylom@users.sourceforge.jp>
Wed, 24 May 2017 13:54:20 +0000 (22:54 +0900)
src/newslash_web/lib/Newslash/Plugin/ReCaptcha.pm [new file with mode: 0644]
src/newslash_web/lib/Newslash/Web.pm
src/newslash_web/lib/Newslash/Web/Controller/Submission.pm
src/newslash_web/public/js/article-item2.js
src/newslash_web/templates/common/captcha/recaptcha.html.tt2
src/newslash_web/templates/common/header.html.tt2

diff --git a/src/newslash_web/lib/Newslash/Plugin/ReCaptcha.pm b/src/newslash_web/lib/Newslash/Plugin/ReCaptcha.pm
new file mode 100644 (file)
index 0000000..9d36e07
--- /dev/null
@@ -0,0 +1,116 @@
+package Newslash::Plugin::ReCaptcha;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Util qw(dumper);
+use Mojo::UserAgent;
+use Mojo::JSON qw(decode_json);
+
+use Compress::Zlib;
+
+use constant RECAPTCHA_URL => 'https://www.google.com/recaptcha/api/siteverify';
+
+sub register {
+    my ($self, $app, $conf) = @_;
+    $app->helper(recaptcha => sub { state $recaptcha = $self; });
+
+    $app->hook(around_action => sub {
+                   my ($next, $c, $action, $last) = @_;
+                   return $next->() if $c->req->method ne 'POST';
+                   return $next->() if !$c->stash('captcha_check');
+                   return $next->() if $c->req->json('/action') eq 'preview';
+
+                   my $user = $c->stash('user');
+
+                   return $next->() if $user->{is_login};
+
+                   my $token = $c->req->json->{recaptcha_token};
+                   if (!$token) {
+                       on_auth_failed($c, $token);
+                       return;
+                   }
+
+                   # validate token
+                   my $ua  = Mojo::UserAgent->new;
+                   my $param = {
+                                secret => $c->app->config->{ReCaptcha}->{secret_key},
+                                response => $token,
+                                #remoteip => foo,
+                               };
+                   my $url = RECAPTCHA_URL;
+
+                   my $tx = $ua->post($url => form => $param);
+                   if ($tx->error) {
+                       on_server_error($c, $token);
+                       return;
+                   }
+
+                   if ($tx->res->code == 200) {
+                       my $encoding = $tx->res->headers->header('Content-Encoding');
+                       my $resp_body = $tx->res->body;
+                       my $json = decode_json($resp_body);
+                       if ($encoding && $encoding =~ /gzip/) {
+                           # response is gziped
+                           $c->app->log->debug("recaptcha response is gziped: $resp_body");
+                           if (!$json) {
+                               $resp_body = Compress::Zlib::memGunzip($resp_body);
+                               $json = decode_json($resp_body);
+                           }
+                       }
+                       my $success = $json->{success} if $json;
+                       if($success) {
+                           return $next->();
+                       }
+                   }
+                   on_auth_failed($c, $token);
+                   return;
+
+                   # $ua->post($url => form => $param
+                   #           => sub {
+                   #               my ($ua, $tx) = @_;
+                   #               warn(dumper($tx));
+                   #               if ($tx->error) {
+                   #                   on_server_error($c, $token);
+                   #                   return;
+                   #               }
+                   #               if ($tx->res->code == 200) {
+                   #                   my $encoding = $tx->res->headers->header('Content-Encoding');
+                   #                   my $resp_body = $tx->res->body;
+                   #                   warn($resp_body);
+                   #                   #if ($encoding && $encoding =~ /gzip/) {
+                   #                   #    # response is gziped
+                   #                   #    $c->app->log->debug("responce is gziped");
+                   #                   #    $resp_body = Compress::Zlib::memGunzip($resp_body);
+                   #                   #}
+                   #                   my $json = decode_json($resp_body);
+                   #                   my $success = $json->{success} if $json;
+                   #                   if ($success) {
+                   #                       return $next->();
+                   #                   }
+                   #               }
+                   #               on_auth_failed($c, $token);
+                   #               return;
+                   #           });
+               });
+
+}
+
+sub on_auth_failed {
+    my ($c, $token) = @_;
+    # validation error
+    my $url = $c->url_for();
+    $c->app->log->debug("invalid reCaptcha_token: $token for: $url");
+    $c->render(json => { error => 1, reason => "invalid_recaptcha_token", message => "ReCaptcha: validation error" });
+    $c->rendered(400);
+    return;
+}
+
+sub on_server_error {
+    my ($c, $token) = @_;
+    # validation error
+    my $url = $c->url_for();
+    $c->app->log->debug("reCaptcha server error: $token for $url");
+    $c->render(json => { error => 1, reason => "recaptcha_server_error", message => "ReCaptcha: server error" });
+    $c->rendered(400);
+    return;
+}
+
+1;
index f59dc30..e95a6c0 100644 (file)
@@ -49,7 +49,7 @@ sub startup {
     $app->secrets([$app->config->{System}->{secret_key},]);
 
     # stash for plugins
-    $app->config->{_Plugins} = {};
+    #$app->config->{_Plugins} = {};
 
     # use BasicAuth?
     if ($app->config->{BasicAuth} && $app->config->{BasicAuth}->{enable}) {
@@ -89,6 +89,9 @@ sub startup {
     # access control
     $app->plugin('Newslash::Plugin::AccessControl');
 
+    # ReCaptcha control
+    $app->plugin('Newslash::Plugin::ReCaptcha');
+
     # Event Que
     $app->plugin('Newslash::Plugin::EventQue');
 
@@ -152,7 +155,7 @@ sub startup {
 
     # User Register
     $r->get('/my/newuser')->to('login#newuser');
-    $r->post('/my/newuser')->to('login#newuser');
+    $r->post('/my/newuser')->to('login#newuser', captcha_check => 1);
 
     # story page
     $r->get('/story/:sid/' => [sid => qr|\d\d/\d\d/\d\d/\d+|])
@@ -163,7 +166,7 @@ sub startup {
     $r->get('/journal/:id/')->to('journal#journal');
 
     # submission page
-    $r->get('/submission/new')->to('submission#create', use_captcha => 1);
+    $r->get('/submission/new')->to('submission#create');
     $r->get('/submission/:id/')->to('submission#submission');
     #$r->post('/submission')->to('submission#create');
 
@@ -198,24 +201,25 @@ sub startup {
     $api->post('/login')->to('API::Login#login');
 
     $api->get('/comment')->to('API::Comment#get');
-    $api->post('/comment')->to('API::Comment#post');
+    $api->post('/comment')->to('API::Comment#post', captcha_check => 1);
 
     $api->get('/user')->to('API::User#get');
     $api->post('/user')->to('API::User#post', seclev => 1);
 
     $api->get('/journal')->to('API::Journal#get');
-    $api->post('/journal')->to('API::Journal#post', seclev => 1);
+    $api->post('/journal')->to('API::Journal#post', seclev => 1, csrf_check_id => 'journal');
 
     $api->get('/submission')->to('API::Submission#get');
-    $api->post('/submission')->to('API::Submission#post');
+    $api->post('/submission')->to('API::Submission#post', captcha_check => 1, csrf_check_id => 'submission');
+
     $api->get('/story')->to('API::Story#get');
-    $api->post('/story')->to('API::Story#post');
+    #$api->post('/story')->to('API::Story#post');
 
+    $api->get('/moderation')->to('API::Moderation#get');
     $api->post('/moderation')->to('API::Moderation#post', seclev => 1, csrf_check_id => 'moderation');
-    $api->get('/moderation')->to('API::Moderation#get',, csrf_check_id => 'moderation');
 
+    $api->get('/metamoderation')->to('API::Metamoderation#get');
     $api->post('/metamoderation')->to('API::Metamoderation#post', seclev => 1, csrf_check_id => 'moderation');
-    $api->get('/metamoderation')->to('API::Metamoderation#get', csrf_check_id => 'moderation');
 
     $api->post('/relation')->to('API::Relation#post', seclev => 1, csrf_check_id => 'relation');
 
index 3911ce7..765cd7f 100644 (file)
@@ -25,7 +25,7 @@ sub create {
     my $c = shift;
 
     if ($c->req->method eq 'GET') {
-        $c->render();
+        $c->render(use_captcha => 1);
         return;
     }
     else {
index 624e948..6c62be9 100644 (file)
@@ -49,9 +49,10 @@ articleItem.init = function init () {
       default: function () {return new Contents();},
     },
     showEditor: {
-      type: Boolean,
+      type: [Boolean, Number],
       default: false,
-    }
+    },
+    csrfToken: String,
   };
 
   var watch = {
@@ -125,7 +126,7 @@ articleItem.init = function init () {
   }
 
   function showPreview(event) {
-    if (!articleItem.token) {
+    if (!articleItem.recaptcha_token) {
       grecaptcha.execute();
       statusBar.init();
       statusBar.loading("checking if you are not a bot...");
@@ -133,13 +134,19 @@ articleItem.init = function init () {
 
     var url = this.urls[this.item.content_type];
     var postData = {};
+    postData.item = {};
+    postData.action = "preview";
+    if (this.csrfToken) {
+      postData.csrf_token = this.csrfToken;
+    }
+
     Object.keys(this.item).forEach(k => {
       // remove circular reference
       if (k != 'editor') {
-        postData[k] = this.item[k];
+        postData.item[k] = this.item[k];
       }
     });
-    this.$http.post(url, {item: postData, action: 'preview'}).then(
+    this.$http.post(url, postData).then(
       (response) => { // success
         this.message = "";
         this.previewTitle = response.body.item.title;
@@ -155,50 +162,81 @@ articleItem.init = function init () {
     );
   }
 
-  function postItem(event) {
-    if (!user.is_login && !articleItem.token) {
+  function _doPost(vm, url, postData, csrfToken, retry) {
+    retry = retry || 0;
+
+    if (retry > 2) {
+      vm.message = "Error: invalid Anti-csrf token";
       return;
     }
-    
-    // load captcha
-    this.message = "";
-    var url = this.urls[this.item.content_type];
-    var postData = {};
-    Object.keys(this.item).forEach(k => {
-      // remove circular reference
-      if (k != 'editor') {
-        postData[k] = this.item[k];
-      }
-    });
-    postData.tags_string = this.tagsString;
-    postData.related_urls = this.relatedUrls;
 
-    if (articleItem.token) {
-        postData.recaptcha_token = articleItem.token;
+    if (csrfToken) {
+      postData.csrf_token = csrfToken;
     }
 
-    this.$http.post(url, {item: postData, action: 'post'}).then(
+    vm.$http.post(url, postData).then(
       (response) => { // success
-        this.message = "";
+        vm.message = "";
         var id = response.body.id;
         var type = response.body.type;
         var url = '/' + type + '/' + id;
         if (!postData.id) { // new post
-          this.createdUrl = url;
-          this.message = "post_success";
+          vm.createdUrl = url;
+          vm.message = "post_success";
         }
-        this.editing = false;
-        this.showSubmit = false;
-        this.showForm = true;
+        vm.editing = false;
+        vm.showSubmit = false;
+        vm.showForm = true;
       },
       (response) => { // fail
-        if (response.body.message) {
-          this.message = response.body.message;
+        if (response.body && response.body.message) {
+          if (response.body.reason == "invalid_csrf_token") {
+            var newToken = response.body.csrf_token;
+            if (newToken) {
+              // retry with new token
+              _doPost(vm, url, postData, newToken, retry + 1);
+              return;
+            }
+          }
+          vm.message = response.body.message;
+        } else {
+          vm.message = "something wrong...";
         }
       }
     );
   }
 
+  function postItem(event) {
+    if (!user.is_login && !articleItem.recaptcha_token) {
+      return;
+    }
+    
+    // load captcha
+    this.message = "";
+    var url = this.urls[this.item.content_type];
+    var postData = {};
+    postData.item = {};
+    postData.action = "post";
+
+    if (articleItem.recaptcha_token) {
+      postData.recaptcha_token = articleItem.recaptcha_token;
+    }
+    if (this.csrfToken) {
+      postData.csrf_token = this.csrfToken;
+    }
+
+    Object.keys(this.item).forEach(k => {
+      // remove circular reference
+      if (k != 'editor') {
+        postData.item[k] = this.item[k];
+      }
+    });
+    postData.item.tags_string = this.tagsString;
+    postData.item.related_urls = this.relatedUrls;
+
+    _doPost(this, url, postData);
+  }
+
   function leavePreview(event) {
     this.previewTitle = "";
     this.previewIntro = "";
@@ -380,7 +418,6 @@ articleItem.init();
 
 function recaptchaDone(token) {
   statusBar.hide();
-  articleItem.token = token;
-  console.log(token);
+  articleItem.recaptcha_token = token;
 }
   
index 206bfd3..52430d9 100644 (file)
@@ -1,8 +1,9 @@
 [%- IF use_captcha && !user.is_login -%]
 <div class="g-recaptcha"
-     data-sitekey="6LcPlyIUAAAAAN2d4Gw4q2DWLtFCDYmn5tSVqS3w"
+     data-sitekey="[% ReCaptcha.site_key %]"
      data-callback="recaptchaDone"
      data-size="invisible">
 </div>
+<script src="https://www.google.com/recaptcha/api.js"></script>
 [%- END -%]
 
index e010631..e294cd2 100644 (file)
@@ -21,7 +21,4 @@
   <script src="/js/escape-html.js" ></script>
   <script src="[% NS.static_content('js/siteconfig.js'); %]" ></script>
 
-  [%- IF use_captcha && !user.is_login -%]
-  <script src="https://www.google.com/recaptcha/api.js"></script>
-  [%- END -%]
 </head>