From 7bfacf09f22df749224bca66340625d5d06ddee3 Mon Sep 17 00:00:00 2001 From: Tharuja Date: Mon, 13 Jan 2020 13:56:06 +0530 Subject: [PATCH] Add persistent cookies --- .../ballerina/src/http/client_endpoint.bal | 9 +- .../ballerina/src/http/cookie/cookieStore.bal | 105 ++++++--- .../default_persistent_cookie_handler.bal | 218 ++++++++++++++++++ .../http/cookie/persistent_cookie_handler.bal | 40 ++++ .../src/http/redirect/redirect_client.bal | 2 +- 5 files changed, 335 insertions(+), 39 deletions(-) create mode 100644 stdlib/http/src/main/ballerina/src/http/cookie/default_persistent_cookie_handler.bal create mode 100644 stdlib/http/src/main/ballerina/src/http/cookie/persistent_cookie_handler.bal diff --git a/stdlib/http/src/main/ballerina/src/http/client_endpoint.bal b/stdlib/http/src/main/ballerina/src/http/client_endpoint.bal index fef26446e33b..d4bb10c4b52e 100644 --- a/stdlib/http/src/main/ballerina/src/http/client_endpoint.bal +++ b/stdlib/http/src/main/ballerina/src/http/client_endpoint.bal @@ -51,7 +51,7 @@ public type Client client object { var cookieConfigVal = self.config.cookieConfig; if (cookieConfigVal is CookieConfig) { if (cookieConfigVal.enabled) { - self.cookieStore = new; + self.cookieStore = new(cookieConfigVal?.persistentStore); } } var result = initialize(url, self.config, self.cookieStore); @@ -374,15 +374,16 @@ public type OutboundAuthConfig record {| # + maxCookiesPerDomain - Maximum number of cookies per domain, which is 50 # + maxTotalCookieCount - Maximum number of total cookies allowed to be stored in cookie store, which is 3000 # + blockThirdPartyCookies - User can block cookies from third party responses and refuse to send cookies for third party requests, if needed -# + enablePersistence - Users are provided with a mechanism for enabling or disabling persistent cookies, which are stored until a specific expiration date. -# If false, only session cookies are used +# + persistentStore - To manage persistent cookies, users are provided with a mechanism for specifying a persistent cookie store with thier own mechanism +# which references the persistent cookie handler or specifying a file path name to be used in the default persistent cookie handler. +# If not specified any, only session cookies are used public type CookieConfig record {| boolean enabled = false; int maxSizePerCookie = 4096; int maxCookiesPerDomain = 50; int maxTotalCookieCount = 3000; boolean blockThirdPartyCookies = true; - boolean enablePersistence = false; + PersistentCookieHandler | string persistentStore?; |}; function initialize(string serviceUrl, ClientConfiguration config, CookieStore? cookieStore) returns HttpClient|error { diff --git a/stdlib/http/src/main/ballerina/src/http/cookie/cookieStore.bal b/stdlib/http/src/main/ballerina/src/http/cookie/cookieStore.bal index 111e512292d3..e0288d620005 100644 --- a/stdlib/http/src/main/ballerina/src/http/cookie/cookieStore.bal +++ b/stdlib/http/src/main/ballerina/src/http/cookie/cookieStore.bal @@ -15,13 +15,25 @@ // under the License. import ballerina/time; +import ballerina/log; # Represents the cookie store. # # + allSessionCookies - Array to store all the session cookies +# + persistentCookieHandler - Persistent cookie handler to manage persistent cookies public type CookieStore object { Cookie[] allSessionCookies = []; + PersistentCookieHandler? persistentCookieHandler = (); + + public function __init(PersistentCookieHandler | string? persistentStore) { + if (persistentStore is string) { + self.persistentCookieHandler = new DefaultPersistentCookieHandler(persistentStore); + } + if (persistentStore is PersistentCookieHandler) { + self.persistentCookieHandler = persistentStore; + } + } # Adds a cookie to the cookie store according to the rules in [RFC-6265](https://tools.ietf.org/html/rfc6265#section-5.3). # @@ -37,7 +49,7 @@ public type CookieStore object { path = requestPath.substring(0,index); } lock { - Cookie? identicalCookie = getIdenticalCookie(cookie, self.allSessionCookies); + Cookie? identicalCookie = getIdenticalCookie(cookie, self); if (!isDomainMatched(cookie, domain, cookieConfig)) { return; } @@ -51,10 +63,10 @@ public type CookieStore object { return; } if (cookie.isPersistent()) { - if (!cookieConfig.enablePersistence) { - return; + var persistentCookieHandler = self.persistentCookieHandler; + if (persistentCookieHandler is PersistentCookieHandler) { + addPersistentCookie(identicalCookie, cookie, url, persistentCookieHandler, self); } - addPersistentCookie(identicalCookie, cookie, url, self); } else { addSessionCookie(identicalCookie, cookie, url, self); } @@ -86,9 +98,12 @@ public type CookieStore object { if (index is int) { path = requestPath.substring(0,index); } + Cookie[] allCookies = self.getAllCookies(); lock { - // Gets the session cookies. - foreach var cookie in self.allSessionCookies { + foreach var cookie in allCookies { + if (isExpired(cookie)) { + continue; + } if (!((url.startsWith(HTTPS) && cookie.secure) || cookie.secure == false)) { continue; } @@ -114,7 +129,18 @@ public type CookieStore object { # # + return - Array of all the cookie objects public function getAllCookies() returns Cookie[] { - return self.allSessionCookies; + var persistentCookieHandler = self.persistentCookieHandler; + Cookie[] allCookies = []; + foreach var cookie in self.allSessionCookies { + allCookies.push(cookie); + } + if (persistentCookieHandler is PersistentCookieHandler) { + Cookie[] persistentCookies = persistentCookieHandler.getCookies(); + foreach var cookie in persistentCookies { + allCookies.push(cookie); + } + } + return allCookies; } # Removes a specific cookie. @@ -124,29 +150,40 @@ public type CookieStore object { # + path - Path of the cookie to be removed # + return - Return true if the relevant cookie is removed, false otherwise public function removeCookie(string name, string domain, string path) returns boolean { - lock { - // Removes the session cookie in the cookie store, which is matched with the given name, domain and path. - int k = 0; - while (k < self.allSessionCookies.length()) { - if (name == self.allSessionCookies[k].name && domain == self.allSessionCookies[k].domain && path == self.allSessionCookies[k].path) { - int j = k; - while (j < self.allSessionCookies.length()-1) { - self.allSessionCookies[j] = self.allSessionCookies[j + 1]; - j = j + 1; - } - _ = self.allSessionCookies.pop(); - return true; - } - k = k + 1; - } - return false; - } + lock { + // Removes the session cookie if it is in the session cookies array, which is matched with the given name, domain and path. + int k = 0; + while (k < self.allSessionCookies.length()) { + if (name == self.allSessionCookies[k].name && domain == self.allSessionCookies[k].domain && path == self.allSessionCookies[k].path) { + int j = k; + while (j < self.allSessionCookies.length()-1) { + self.allSessionCookies[j] = self.allSessionCookies[j + 1]; + j = j + 1; + } + _ = self.allSessionCookies.pop(); + return true; + } + k = k + 1; + } + // Removes the persistent cookie if it is in the persistent cookie store, which is matched with the given name, domain and path. + var persistentCookieHandler = self.persistentCookieHandler; + if (persistentCookieHandler is PersistentCookieHandler) { + return persistentCookieHandler.removeCookie(name, domain, path); + } else { + log:printError("No such cookie to remove"); + return false; + } + } } # Removes all the cookies. public function clear() { + var persistentCookieHandler = self.persistentCookieHandler; lock { self.allSessionCookies = []; + if (persistentCookieHandler is PersistentCookieHandler) { + persistentCookieHandler.clearAllCookies(); + } } } }; @@ -177,14 +214,14 @@ function getDomain(string url) returns string { # Identical cookie is the cookie, which has the same name, domain and path as the given cookie. # # + cookieToCompare - Cookie to be compared -# + allSessionCookies - Array which stores all the session cookies +# + cookieStore - Cookie store of the client # + return - Identical cookie if one exists, else `()` -function getIdenticalCookie(Cookie cookieToCompare, Cookie[] allSessionCookies) returns Cookie? { - // Searches for the session cookies. +function getIdenticalCookie(Cookie cookieToCompare, CookieStore cookieStore) returns Cookie? { + Cookie[] allCookies = cookieStore.getAllCookies(); int k = 0 ; - while (k < allSessionCookies.length()) { - if (cookieToCompare.name == allSessionCookies[k].name && cookieToCompare.domain == allSessionCookies[k].domain && cookieToCompare.path == allSessionCookies[k].path) { - return allSessionCookies[k]; + while (k < allCookies.length()) { + if (cookieToCompare.name == allCookies[k].name && cookieToCompare.domain == allCookies[k].domain && cookieToCompare.path == allCookies[k].path) { + return allCookies[k]; } k = k + 1; } @@ -262,7 +299,7 @@ function isExpiresAttributeValid(Cookie cookie) returns boolean { } // Adds a persistent cookie to the cookie store according to the rules in [RFC-6265](https://tools.ietf.org/html/rfc6265#section-5.3 , https://tools.ietf.org/html/rfc6265#section-4.1.2). -function addPersistentCookie(Cookie? identicalCookie, Cookie cookie, string url, CookieStore cookieStore) { +function addPersistentCookie(Cookie? identicalCookie, Cookie cookie, string url, PersistentCookieHandler persistentCookieHandler, CookieStore cookieStore) { if (identicalCookie is Cookie) { var temp1 = identicalCookie.name; var temp2 = identicalCookie.domain; @@ -275,7 +312,7 @@ function addPersistentCookie(Cookie? identicalCookie, Cookie cookie, string url, _ = cookieStore.removeCookie(temp1, temp2, temp3); cookie.creationTime = identicalCookie.creationTime; cookie.lastAccessedTime = time:currentTime(); - // TODO:insert into the database. + persistentCookieHandler.storeCookie(cookie); } } } else { @@ -283,7 +320,7 @@ function addPersistentCookie(Cookie? identicalCookie, Cookie cookie, string url, if (!isExpired(cookie)) { cookie.creationTime = time:currentTime(); cookie.lastAccessedTime = time:currentTime(); - // TODO:insert into the database. + persistentCookieHandler.storeCookie(cookie); } } } @@ -322,7 +359,7 @@ function addSessionCookie(Cookie? identicalCookie, Cookie cookie, string url, Co cookie.creationTime = identicalCookie.creationTime; cookie.lastAccessedTime = time:currentTime(); cookieStore.allSessionCookies.push(cookie); - } + } } else { // Adds the session cookie. cookie.creationTime = time:currentTime(); diff --git a/stdlib/http/src/main/ballerina/src/http/cookie/default_persistent_cookie_handler.bal b/stdlib/http/src/main/ballerina/src/http/cookie/default_persistent_cookie_handler.bal new file mode 100644 index 000000000000..aa32025c7a10 --- /dev/null +++ b/stdlib/http/src/main/ballerina/src/http/cookie/default_persistent_cookie_handler.bal @@ -0,0 +1,218 @@ +// Copyright (c) 2020 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/log; +import ballerina/file; +import ballerina/time; + +type myCookie record { + string name; + string value; + string domain; + string path; + string expires; + int maxAge; + boolean httpOnly; + boolean secure; + string creationTime; + string lastAccessedTime; + boolean hostOnly; +}; + +string? cookieNameToRemove = (); +string? cookieDomainToRemove = (); +string? cookiePathToRemove = (); + +public type DefaultPersistentCookieHandler object { + *PersistentCookieHandler; + + string fileName = ""; + + public function __init(string fileName) { + self.fileName = fileName + ".csv"; + } + # Adds a persistent cookie to the cookie store. + # + # + cookie - Cookie to be added + public function storeCookie(Cookie cookie) { + table cookiesTable = table{}; + if (file:exists(self.fileName)) { + cookiesTable = getFileDataIntoTable(self.fileName); + } + cookiesTable = addNewCookieToTable(cookiesTable, cookie); + var result = writeToFile(cookiesTable, self.fileName); + if (result is error) { + log:printError("Error occurred while writing data to file: ", err = result); + } + } + + # Gets all persistent cookies. + # + # + return - Array of persistent cookies stored in the cookie store + public function getCookies() returns Cookie[] { + Cookie[] cookies = []; + if (file:exists(self.fileName)) { + var tblResult = readFile(self.fileName); + if (tblResult is table) { + foreach var rec in tblResult { + Cookie cookie = new(rec.name, rec.value); + cookie.domain = rec.domain; + cookie.path = rec.path; + cookie.expires = rec.expires; + cookie.maxAge = rec.maxAge; + cookie.httpOnly = rec.httpOnly; + cookie.secure = rec.secure; + time:Time | error t1 = time:parse(rec.creationTime, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + if (t1 is time:Time) { + cookie.creationTime = t1; + } + time:Time | error t2 = time:parse(rec.lastAccessedTime, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + if (t2 is time:Time) { + cookie.lastAccessedTime = t2; + } + cookie.hostOnly = rec.hostOnly; + cookies.push(cookie); + } + } else { + log:printError("An error occurred while reading file: ", err = tblResult); + } + } + return cookies; + } + + # Removes a specific persistent cookie. + # + # + name - Name of the persistent cookie to be removed + # + domain - Domain of the persistent cookie to be removed + # + path - Path of the persistent cookie to be removed + # + return - Return true if the relevant persistent cookie is removed, false otherwise + public function removeCookie(string name, string domain, string path) returns boolean { + cookieNameToRemove = name; + cookieDomainToRemove = domain; + cookiePathToRemove = path; + if (file:exists(self.fileName)) { + table cookiesTable = getFileDataIntoTable(self.fileName); + int | error count = cookiesTable.remove(checkRemoveCriteria); + if (count is error || count <= 0) { + log:printError("No such cookie to remove"); + return false; + } + error? removeResults = file:remove(self.fileName); + if (removeResults is error) { + log:printError("Error occurred while removing the existing file"); + return false; + } + var result = writeToFile(cookiesTable, self.fileName); + if (result is error) { + log:printError("Error occurred while writing updated table to file"); + return false; + } + return true; + } + log:printError("No persistent cookie store file to remove cookie"); + return false; + } + + # Removes all persistent cookies. + public function clearAllCookies() { + error? removeResults = file:remove(self.fileName); + if (removeResults is error) { + log:printError("Error occurred while removing the persistent cookie store file: ", err = removeResults); + } + } +}; + +// Reads file and gets earlier data into a table. +function getFileDataIntoTable(string fileName) returns @tainted table { + table cookiesTable = table{}; + var tblResult = readFile(fileName); + if (tblResult is table) { + cookiesTable = tblResult; + } else { + log:printError("An error occurred while reading file: ", err = tblResult); + } + return cookiesTable; +} + +function readFile(string fileName) returns @tainted error | table { + io:ReadableCSVChannel rCsvChannel2 = check io:openReadableCsvFile(fileName); + var tblResult = rCsvChannel2.getTable(myCookie); + closeReadableCSVChannel(rCsvChannel2); + return tblResult; +} + +function closeReadableCSVChannel(io:ReadableCSVChannel csvChannel) { + var result = csvChannel.close(); + if (result is error) { + log:printError("Error occurred while closing the channel: ", err = result); + } +} + +// Updates the table with new cookie. +function addNewCookieToTable(table cookiesTable, Cookie cookieToAdd) returns table { + table tableToReturn = cookiesTable; + var name = cookieToAdd.name; + var value = cookieToAdd.value; + var domain = cookieToAdd.domain; + var path = cookieToAdd.path; + var expires = cookieToAdd.expires; + var creationTime = time:format(cookieToAdd.creationTime, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + var lastAccessedTime = time:format(cookieToAdd.lastAccessedTime, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + if (name is string && value is string && domain is string && path is string && expires is string && creationTime is string && lastAccessedTime is string) { + myCookie c1 = { name: name, value: value, domain: domain, path: path, expires: expires, maxAge: cookieToAdd.maxAge, httpOnly: cookieToAdd.httpOnly, secure: cookieToAdd.secure, creationTime: creationTime, lastAccessedTime: lastAccessedTime, hostOnly: cookieToAdd.hostOnly }; + var result = tableToReturn.add(c1); + if (result is error) { + log:printError("Error occurred while adding data to table: ", err = result); + } + } else { + log:printError("Error occurred while adding data to table"); + } + return tableToReturn; +} + +// Writes the updated table to file. +function writeToFile(table cookiesTable, string fileName) returns @tainted error? { + io:WritableCSVChannel wCsvChannel2 = check io:openWritableCsvFile(fileName); + foreach var entry in cookiesTable { + string[] rec = [entry.name, entry.value, entry.domain, entry.path, entry.expires, entry.maxAge.toString(), entry.httpOnly.toString(), entry.secure.toString(), entry.creationTime, entry.lastAccessedTime, entry.hostOnly.toString()]; + writeDataToCSVChannel(wCsvChannel2, rec); + } + closeWritableCSVChannel(wCsvChannel2); +} + +function writeDataToCSVChannel(io:WritableCSVChannel csvChannel, string[]... data) { + foreach var rec in data { + var returnedVal = csvChannel.write(rec); + if (returnedVal is error) { + log:printError("Error occurred while writing to target file: ", err = returnedVal); + } + } +} + +function closeWritableCSVChannel(io:WritableCSVChannel csvChannel) { + var result = csvChannel.close(); + if (result is error) { + log:printError("Error occurred while closing the channel: ", err = result); + } +} + +function checkRemoveCriteria(myCookie rec) returns boolean { + if (rec.name == cookieNameToRemove && rec.domain == cookieDomainToRemove && rec.path == cookiePathToRemove) { + return true; + } + return false; +} diff --git a/stdlib/http/src/main/ballerina/src/http/cookie/persistent_cookie_handler.bal b/stdlib/http/src/main/ballerina/src/http/cookie/persistent_cookie_handler.bal new file mode 100644 index 000000000000..c515b901f1bf --- /dev/null +++ b/stdlib/http/src/main/ballerina/src/http/cookie/persistent_cookie_handler.bal @@ -0,0 +1,40 @@ +// Copyright (c) 2020 WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +# The representation of a persistent cookie handler for managing persistent cookies. +public type PersistentCookieHandler abstract object { + + # Adds a persistent cookie to the cookie store. + # + # + cookie - Cookie to be added + public function storeCookie(Cookie cookie); + + # Gets all persistent cookies. + # + # + return - Array of persistent cookies stored in the cookie store + public function getCookies() returns Cookie[]; + + # Removes a specific persistent cookie. + # + # + name - Name of the persistent cookie to be removed + # + domain - Domain of the persistent cookie to be removed + # + path - Path of the persistent cookie to be removed + # + return - Return true if the relevant persistent cookie is removed, false otherwise + public function removeCookie(string name, string domain, string path) returns boolean; + + # Removes all persistent cookies. + public function clearAllCookies(); +}; diff --git a/stdlib/http/src/main/ballerina/src/http/redirect/redirect_client.bal b/stdlib/http/src/main/ballerina/src/http/redirect/redirect_client.bal index 5d2e4ba92dce..e4823ad877f9 100644 --- a/stdlib/http/src/main/ballerina/src/http/redirect/redirect_client.bal +++ b/stdlib/http/src/main/ballerina/src/http/redirect/redirect_client.bal @@ -331,7 +331,7 @@ function performRedirection(string location, RedirectClient redirectClient, Http var cookieConfigVal = redirectClient.config.cookieConfig; if (cookieConfigVal is CookieConfig) { if (cookieConfigVal.enabled) { - cookieStore = new; + cookieStore = new(cookieConfigVal?.persistentStore); } } var retryClient = createRetryClient(location, createNewEndpointConfig(redirectClient.config), cookieStore);