--- /dev/null
+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;
$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}) {
# access control
$app->plugin('Newslash::Plugin::AccessControl');
+ # ReCaptcha control
+ $app->plugin('Newslash::Plugin::ReCaptcha');
+
# Event Que
$app->plugin('Newslash::Plugin::EventQue');
# 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+|])
$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');
$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');
my $c = shift;
if ($c->req->method eq 'GET') {
- $c->render();
+ $c->render(use_captcha => 1);
return;
}
else {
default: function () {return new Contents();},
},
showEditor: {
- type: Boolean,
+ type: [Boolean, Number],
default: false,
- }
+ },
+ csrfToken: String,
};
var watch = {
}
function showPreview(event) {
- if (!articleItem.token) {
+ if (!articleItem.recaptcha_token) {
grecaptcha.execute();
statusBar.init();
statusBar.loading("checking if you are not a bot...");
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;
);
}
- 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 = "";
function recaptchaDone(token) {
statusBar.hide();
- articleItem.token = token;
- console.log(token);
+ articleItem.recaptcha_token = token;
}
[%- 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 -%]
<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>