nexcess / magento-turpentine

A Varnish extension for Magento.
GNU General Public License v2.0
519 stars 253 forks source link

Issue with AJAX login not actually logging you in #191

Closed csdougliss closed 11 years ago

csdougliss commented 11 years ago

I'm having an issue similar to a previously closed issue - ajax login (but using 0.5.3).

We have a login form that does a POST to our customer controller using AJAX. The user appears to get logged in (I checked using logs) but after a page refresh the user is not logged in.

The login post action is the same as the default magento one except that it returns some information to the page. That returns that the login was succesful but on the page refresh not so. I added our customer controller to the blacklist.

There appears to be some cookies that are not being set:

Name    CUSTOMER
Value   e922ffe2a54cf80ad9c7610c5e741e36
Host    .xxx
Path    /
Expires Thu, 16 May 2013 11:09:37 GMT
Secure  No
HttpOnly    Yes
Name    CUSTOMER_AUTH
Value   9986aa4d124454302361fe442289ae61
Host    .xxx
Path    /
Expires Thu, 16 May 2013 11:09:37 GMT
Secure  No
HttpOnly    Yes
Name    CUSTOMER_INFO
Value   e78d80922fcf885630573e2cb7c28fcd
Host    .xxx
Path    /
Expires Thu, 16 May 2013 11:09:37 GMT
Secure  No
HttpOnly    Yes
public function loginPostAction()
    {
        if ($this->_getSession()->isLoggedIn()) {
                $this->_redirect('*/*/');
                return;
        }
        $session = $this->_getSession();
        $message = "";

        if ($this->getRequest()->isPost()) {
            $login = $this->getRequest()->getPost('login');
            if (!empty($login['username']) && !empty($login['password'])) {
                try {
                    $session->login($login['username'], $login['password']);

                } catch (Mage_Core_Exception $e) {
                    switch ($e->getCode()) {
                        case Mage_Customer_Model_Customer::EXCEPTION_INVALID_EMAIL_OR_PASSWORD:
                            $message = $e->getMessage();
                            break;
                        default:
                            $message = $e->getMessage();
                    }
                    $session->addError($message);
                    $session->setUsername($login['username']);
                } catch (Exception $e) {
                    Mage::logException($e); // PA DSS violation: this exception log can disclose customer password
                }
            } else {
                $message = $this->__('Login and password are required.');
                $session->addError($message);
            }
        }

        /*
         * AJAX response so set the response message and record the customer
         * name if successful
         */
        $response = array('logged_in' => false,
                            'message' => $message);

        if($message == "") {
            $customer = $session->getCustomer();
            $response['logged_in'] = true;
            $response['name'] = $customer->getName();
            $response['group'] = $customer->getGroupId();
        }

        $this->getResponse()->setBody(Zend_Json::encode($response));
    }
aheadley commented 11 years ago

I added our customer controller to the blacklist.

How did you do this exactly? If the login URL was blacklisted it should be piped through Varnish so the extra cookies should be set. If that's not happening I think you may have added it to the blacklist wrong.

csdougliss commented 11 years ago

The login form posts to /vax-customer/account/loginPost/.

This is the content of the VCL file:

C{
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <pthread.h>
static pthread_mutex_t lrand_mutex = PTHREAD_MUTEX_INITIALIZER;
void generate_uuid(char* buf) {
pthread_mutex_lock(&lrand_mutex);
long a = lrand48();
long b = lrand48();
long c = lrand48();
long d = lrand48();
pthread_mutex_unlock(&lrand_mutex);
sprintf(buf, "frontend=%08lx-%04lx-%04lx-%04lx-%04lx%08lx",
a,
b & 0xffff,
(b & ((long)0x0fff0000) >> 16) | 0x4000,
(c & 0x0fff) | 0x8000,
(c & (long)0xffff0000) >> 16,
d
);
return;
}
}C
import std;
C{
#include <syslog.h>
#include <stddef.h>
}C
sub vcl_recv {
if (req.restarts == 0) {
C{
struct timeval detail_time;
gettimeofday(&detail_time,NULL);
char start[20];
sprintf(start, "t=%lu%06lu", detail_time.tv_sec, detail_time.tv_usec);
VRT_SetHdr(sp, HDR_REQ, "\020X-Request-Start:", start, vrt_magic_string_end);
}C
}
if (req.url ~ "^/registration/form") {
return (pass);
}
}
sub vcl_error {
set obj.http.Content-Type = "text/html; charset=utf-8";
set obj.http.Retry-After = "5";
if (obj.status >= 500) {
C{
FILE *fp;
fp = fopen("/var/log/varnish/error_log", "a");
if(fp != NULL) {
fprintf(fp, "Error (%s) (%s) (%s)\n",
VRT_r_req_url(sp), VRT_r_obj_response(sp), VRT_r_req_xid(sp));
fclose(fp);
} else {
syslog(LOG_INFO, "Error (%s) (%s) (%s)",
VRT_r_req_url(sp), VRT_r_obj_response(sp), VRT_r_req_xid(sp));
}
}C
}
synthetic {"
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>"} + obj.status + " " + obj.response + {"</title>
<style type="text/css">
/* Errors */
* {
margin: 0;
padding: 0;
}
.error-layout { width: 100%; max-width: 1600px; height: 100%; margin: 0 auto; padding: 0; }
.error-layout .main-container { height: 100%; }
.error-layout .main { background: url("/errors/default/images/error_background.jpg") no-repeat #fff; min-height: 590px; color: #1D2B33; float: left; }
.error-layout .col-main { width: 100%; }
.error-layout .std { margin: 100px 0 0 400px; padding-left: 270px; background: url("/errors/default/images/warning_icon.png") no-repeat 10px 0; height: 200px; }
.error-layout h3 { font-size: 400%; margin-bottom: 10px; }
.error-layout p { font-size: 180%; }
.error-layout .back { float: left; background: url("/skin/frontend/vax/uk/images/icons/left_arrow.png") no-repeat 8px center #1D2B33; height: 18px; padding: 5px 8px 5px 33px; color: #FFF; font-size: 13px; line-height: 18px; text-decoration: none; margin-top: 10px; }
/* ======================================================================================= */
</style>
</head>
<body>
<div class="page error-layout">
<div class="main-container">
<div class="main">
<div class="col-main">
<div class="std"><h3>Oops, something went wrong</h3>
<!--<h1>Error "} + obj.status + " " + obj.response + {"</h1>
<p>"} + obj.response + {"</p>
<h3>Guru Meditation:</h3>
<p>XID: "} + req.xid + {"</p>
<hr>-->
<p><a class="back" title="Go Back" href="javascript: history.go(-1);">Go Back</a></p>
</div>
</div>
</div>
</div>
</body>
</html>
"};
return (deliver);
}
backend default {
.host = "127.0.0.1";
.port = "8080";
.first_byte_timeout = 300s;
.between_bytes_timeout = 300s;
}
backend admin {
.host = "127.0.0.1";
.port = "8080";
.first_byte_timeout = 21600s;
.between_bytes_timeout = 21600s;
}
acl crawler_acl {
}
acl debug_acl {
"127.0.0.1";
}
sub remove_cache_headers {
unset beresp.http.Cache-Control;
unset beresp.http.Expires;
unset beresp.http.Pragma;
unset beresp.http.Cache;
unset beresp.http.Age;
}
sub remove_double_slashes {
set req.url = regsub(req.url, "(.*)//+(.*)", "\1/\2");
}
sub generate_session {
if (req.url ~ ".*[&?]SID=([^&]+).*") {
set req.http.X-Varnish-Faked-Session = regsub(
req.url, ".*[&?]SID=([^&]+).*", "frontend=\1");
} else {
C{
char uuid_buf [50];
generate_uuid(uuid_buf);
VRT_SetHdr(sp, HDR_REQ,
"\030X-Varnish-Faked-Session:",
uuid_buf,
vrt_magic_string_end
);
}C
}
if (req.http.Cookie) {
std.collect(req.http.Cookie);
set req.http.Cookie = req.http.X-Varnish-Faked-Session +
"; " + req.http.Cookie;
} else {
set req.http.Cookie = req.http.X-Varnish-Faked-Session;
}
}
sub generate_session_expires {
C{
time_t now = time(NULL);
struct tm now_tm = *gmtime(&now);
now_tm.tm_sec += 3600;
mktime(&now_tm);
char date_buf [50];
strftime(date_buf, sizeof(date_buf)-1, "%a, %d-%b-%Y %H:%M:%S %Z", &now_tm);
VRT_SetHdr(sp, HDR_RESP,
"\031X-Varnish-Cookie-Expires:",
date_buf,
vrt_magic_string_end
);
}C
}
sub vcl_recv {
if (req.restarts == 0) {
if (req.http.X-Forwarded-For) {
set req.http.X-Forwarded-For =
req.http.X-Forwarded-For + ", " + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
}
if (req.request !~ "^(GET|HEAD)$") {
return (pipe);
}
call remove_double_slashes;
if (req.http.Accept-Encoding) {
if (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else if (req.http.Accept-Encoding ~ "deflate") {
set req.http.Accept-Encoding = "deflate";
} else {
unset req.http.Accept-Encoding;
}
}
if (!true || req.http.Authorization) {
return (pipe);
}
if (req.url ~ "^(/)(?:(?:index|litespeed)\.php/)?") {
set req.http.X-Turpentine-Secret-Handshake = "1";
if (req.url ~ "^(/)(?:(?:index|litespeed)\.php/)?admin") {
set req.backend = admin;
return (pipe);
}
if (req.http.Cookie ~ "\bcurrency=") {
set req.http.X-Varnish-Currency = regsub(
req.http.Cookie, ".*\bcurrency=([^;]*).*", "\1");
}
if (req.http.Cookie ~ "\bstore=") {
set req.http.X-Varnish-Store = regsub(
req.http.Cookie, ".*\bstore=([^;]*).*", "\1");
}
if (req.url ~ "/turpentine/esi/getBlock/") {
set req.http.X-Varnish-Esi-Method = regsub(
req.url, ".*/method/(\w+)/.*", "\1");
set req.http.X-Varnish-Esi-Access = regsub(
req.url, ".*/access/(\w+)/.*", "\1");
if (req.http.X-Varnish-Esi-Method == "esi" && req.esi_level == 0 &&
!(true || client.ip ~ debug_acl)) {
error 403 "External ESI requests are not allowed";
}
}
if (req.http.Cookie !~ "frontend=") {
if (client.ip ~ crawler_acl ||
req.http.User-Agent ~ "^(?:ApacheBench/.*|.*Googlebot.*|JoeDog/.*Siege.*|magespeedtest\.com|Nexcessnet_Turpentine/.*)$") {
set req.http.Cookie = "frontend=crawler-session";
} else {
call generate_session;
}
}
if (true &&
req.url ~ ".*\.(?:css|js|jpe?g|png|gif|ico|swf)(?=\?|&|$)") {
unset req.http.Cookie;
unset req.http.X-Varnish-Faked-Session;
return (lookup);
}
if (req.url ~ "^(/)(?:(?:index|litespeed)\.php/)?(?:admin|api|cron\.php|registration/form|vax-customer/account/loginPost)") {
return (pipe);
}
if (req.url ~ "\?.*__from_store=") {
return (pipe);
}
if (true &&
req.url ~ "(?:[?&](?:__SID|XDEBUG_PROFILE)(?=[&=]|$))") {
return (pass);
}
if (req.url ~ "[?&](utm_source|utm_medium|utm_campaign|gclid|cx|ie|cof|siteurl)=") {
set req.url = regsuball(req.url, "(?:(\?)?|&)(?:utm_source|utm_medium|utm_campaign|gclid|cx|ie|cof|siteurl)=[^&]+", "\1");
set req.url = regsuball(req.url, "(?:(\?)&|\?$)", "\1");
}
return (lookup);
}
}
sub vcl_pipe {
unset bereq.http.X-Turpentine-Secret-Handshake;
set bereq.http.Connection = "close";
}
sub vcl_hash {
hash_data(req.url);
if (req.http.Host) {
hash_data(req.http.Host);
} else {
hash_data(server.ip);
}
hash_data(req.http.Ssl-Offloaded);
if (req.http.X-Normalized-User-Agent) {
hash_data(req.http.X-Normalized-User-Agent);
}
if (req.http.Accept-Encoding) {
hash_data(req.http.Accept-Encoding);
}
hash_data("s=" + req.http.X-Varnish-Store + "&c=" + req.http.X-Varnish-Currency);
if (req.http.X-Varnish-Esi-Access == "private" &&
req.http.Cookie ~ "frontend=") {
hash_data(regsub(req.http.Cookie, "^.*?frontend=([^;]*);*.*$", "\1"));
hash_data(req.http.User-Agent);
}
return (hash);
}
sub vcl_hit {
}
sub vcl_fetch {
set req.grace = 15s;
if (req.url ~ "^(/)(?:(?:index|litespeed)\.php/)?") {
unset beresp.http.Vary;
set beresp.do_gzip = true;
if (beresp.status != 200 && beresp.status != 404) {
set beresp.ttl = 15s;
return (hit_for_pass);
} else {
if (beresp.http.Set-Cookie) {
set beresp.http.X-Varnish-Set-Cookie = beresp.http.Set-Cookie;
unset beresp.http.Set-Cookie;
}
call remove_cache_headers;
if (beresp.http.X-Turpentine-Esi == "1") {
set beresp.do_esi = true;
}
if (beresp.http.X-Turpentine-Cache == "0") {
set beresp.ttl = 15s;
return (hit_for_pass);
} else {
if (true &&
bereq.url ~ ".*\.(?:css|js|jpe?g|png|gif|ico|swf)(?=\?|&|$)") {
set beresp.ttl = 28800s;
set beresp.http.Cache-Control = "max-age=28800";
} elseif (req.http.X-Varnish-Esi-Method) {
if (req.http.X-Varnish-Esi-Access == "private" &&
req.http.Cookie ~ "frontend=") {
set beresp.http.X-Varnish-Session = regsub(req.http.Cookie,
"^.*?frontend=([^;]*);*.*$", "\1");
}
if (req.http.X-Varnish-Esi-Method == "ajax" &&
req.http.X-Varnish-Esi-Access == "public") {
set beresp.http.Cache-Control = "max-age=" + regsub(
req.url, ".*/ttl/(\d+)/.*", "\1");
}
set beresp.ttl = std.duration(
regsub(
req.url, ".*/ttl/(\d+)/.*", "\1s"),
300s);
if (beresp.ttl == 0s) {
set beresp.ttl = 15s;
return (hit_for_pass);
}
} else {
set beresp.ttl = 3600s;
}
}
}
return (deliver);
}
}
sub vcl_deliver {
if (req.http.X-Varnish-Faked-Session) {
call generate_session_expires;
set resp.http.Set-Cookie = req.http.X-Varnish-Faked-Session +
"; expires=" + resp.http.X-Varnish-Cookie-Expires + "; path=/";
if (req.http.Host) {
set resp.http.Set-Cookie = resp.http.Set-Cookie +
"; domain=" + regsub(req.http.Host, ":\d+$", "");
}
set resp.http.Set-Cookie = resp.http.Set-Cookie + "; httponly";
unset resp.http.X-Varnish-Cookie-Expires;
}
if (true || client.ip ~ debug_acl) {
set resp.http.X-Varnish-Hits = obj.hits;
set resp.http.X-Varnish-Esi-Method = req.http.X-Varnish-Esi-Method;
set resp.http.X-Varnish-Esi-Access = req.http.X-Varnish-Esi-Access;
set resp.http.X-Varnish-Currency = req.http.X-Varnish-Currency;
set resp.http.X-Varnish-Store = req.http.X-Varnish-Store;
} else {
unset resp.http.X-Varnish;
unset resp.http.Via;
unset resp.http.X-Powered-By;
unset resp.http.Server;
unset resp.http.X-Turpentine-Cache;
unset resp.http.X-Turpentine-Esi;
unset resp.http.X-Turpentine-Flush-Events;
unset resp.http.X-Turpentine-Block;
unset resp.http.X-Varnish-Session;
unset resp.http.X-Varnish-Set-Cookie;
}
}
csdougliss commented 11 years ago

OK what is rather odd is that the magento normal login does not work. However, a different store using the same code does. I'm actually using the devel branch of turpentine (not 0.5.3!)

csdougliss commented 11 years ago

I have found the issue/solution. The store has a cookie domain set as .vaxuk.local (notice the dot at the front). We do this to share sessions/carts between sub-domains e.g. support.vaxuk.local (this is on my dev machine).

When I remove the dot, the login works normally.

Is this something that can be catered for in the vcl? The magento setup does mention using . for sub-domains is acceptable.

What's odd is that when I remove the dot and set the cookie domain to vaxuk.local in the admin, after clearing the cache and clearing my cookies.. the cookie domain is still .vaxuk.local. However the login works?

aheadley commented 11 years ago

The cookie that Varnish creates is based on the Host header in the request (i.e. if you send the request to www.example.com the domain for the cookie will be www.example.com), Turpentine doesn't use the cookie setting in Magento because the VCL it generates doesn't really handle different settings per store yet. You can force Varnish to set the cookie with the domain as .vaxuk.local by changing these lines in the VCL template:

        if (req.http.Host) {
            set resp.http.Set-Cookie = resp.http.Set-Cookie +
                "; domain=" + regsub(req.http.Host, ":\d+$", "");
        }

to this:

        set resp.http.Set-Cookie = resp.http.Set-Cookie + "; domain=.vaxuk.local";

As a side note, IIRC using a leading . (dot) in the cookie domain makes it valid for any subdomain but not the base domain. I could be wrong about this though, but it may be related to your issue.

csdougliss commented 11 years ago

Thanks, I believe the problem only occurs locally and on my dev server, because on live the user gets re-redirected to www (hence why I never have the problem on a production server!).

csdougliss commented 11 years ago

I tested it on the production server and the result was unexpected.

Turpentine VCL created a frontend cookie with a domain name of .www.ourhost.etc But I also got a second frontend cookie created with a domain name of .ourhost.etc (as per magento settings)!

The login functionality didn't work.

I can't set the domain name in the VCL as we have multiple sites/hosts. Is there a way to strip out the www perhaps so it matches magentos? Unless in the VCL it is possible to setup a specific case, so it only strips out the www when it matches a certain string?

I can't hard code the domain name in the VCL because there are multi-stores with different URLs

aheadley commented 11 years ago

This should strip the "www." from the cookie domain:

diff --git a/app/code/community/Nexcessnet/Turpentine/misc/version-3.vcl b/app/code/community/Nexcessnet/Turpentine/misc/version-3.vcl
index 72cd460..2d8c1bf 100644
--- a/app/code/community/Nexcessnet/Turpentine/misc/version-3.vcl
+++ b/app/code/community/Nexcessnet/Turpentine/misc/version-3.vcl
@@ -346,7 +346,7 @@ sub vcl_deliver {
             "; expires=" + resp.http.X-Varnish-Cookie-Expires + "; path=/";
         if (req.http.Host) {
             set resp.http.Set-Cookie = resp.http.Set-Cookie +
-                "; domain=" + regsub(req.http.Host, ":\d+$", "");
+                "; domain=" + regsub(req.http.Host, "(?:www\.)?([^:]*)(?::\d+)?$", "\1");
         }
         set resp.http.Set-Cookie = resp.http.Set-Cookie + "; httponly";
         unset resp.http.X-Varnish-Cookie-Expires;

You may need to fiddle with the regex to get it working exactly how you want.

csdougliss commented 11 years ago

Thanks for that, very useful. I think I will have to add a check, say if req.http.host contains vax then remove the www, spares sub-domains etc.

I was wondering - regarding the normalise host option in the admin - would it be possible to make it so that only normalizes if the req.http.host matches a certain regex? (matching all if nothing supplied)? e.g. say normalize host is turned on, the cookie domain will be set to the host_target (e.g. .vaxuk.local) if the host regex matches www.vaxuk.local, spares.vaxuk.local etc?

aheadley commented 11 years ago

I was wondering - regarding the normalise host option in the admin - would it be possible to make it so that only normalizes if the req.http.host matches a certain regex?

It's not impossible but it's not something I want to add to Turpentine.

csdougliss commented 11 years ago

This was my solution to the problem. It appears to be a necessary one if you are sharing cookie domains with multiple stores!. It may not be the best solution, but it works for now. Another option might be instead of setting the domain to the normalize_cookie_target just have it remove whatever the sub-domain was. I might go back and change that when I get a chance.

In vcl_deliver

if (req.http.Host) {
            if(req.http.Host ~ "{{normalize_cookie_regex}}") {
                set resp.http.Set-Cookie = resp.http.Set-Cookie +
                    "; domain={{normalize_cookie_target}}";
            } else {
                set resp.http.Set-Cookie = resp.http.Set-Cookie +
                    "; domain=" + regsub(req.http.Host, ":\d+$", "");
            }
        }

Then in Nexcessnet_Turpentine_Model_Varnish_Configurator_Version3 I added

/**
     * Get the hostname for cookie normalization
     *
     * @return string
     */
    protected function _getNormalizeCookieTarget() {
        return trim( Mage::getStoreConfig(
            'turpentine_vcl/normalization/cookie_target' ) );
    }

    /**
     * Get the regex for cookie normalization
     *
     * @return string
     */
    protected function _getNormalizeCookieRegex() {
        return trim( Mage::getStoreConfig(
            'turpentine_vcl/normalization/cookie_regex' ) );
    }

In _getTemplateVars()

if( Mage::getStoreConfig( 'turpentine_vcl/normalization/cookie_regex' ) ) {
            $vars['normalize_cookie_regex'] = $this->_getNormalizeCookieRegex();
        }
        if( Mage::getStoreConfig( 'turpentine_vcl/normalization/cookie_target' ) ) {
            $vars['normalize_cookie_target'] = $this->_getNormalizeCookieTarget();
        }
aheadley commented 11 years ago

Glad you got something that works for you.