CookieJar.java
/*
** Module : CookieJar.java
** Abstract : Implementation of the builtin class.
**
** Copyright (c) 2019-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description--------------------------------
** 001 IAS 20190923 First version.
** 002 ME 20200410 Implement ILogger interface fully.
** 003 MP 20200611 Complete class with stubs taken by converting the skeleton using FWD.
** 004 ME 20201028 Implement all methods as per OE12.2.
** CA 20210221 Fixed parameters at the LegacySignature annotation.
** 005 CA 20210221 Fixed 'qualified', 'extent' and 'returns' annotations at the legacy
** signature.
** CA 20210609 Updated INPUT/INPUT-OUTPUT parameters to the new approach, where they are explicitly
** initialized at the method's execution, and not at the caller's arguments.
** ME 20211012 Increment/decrement object references on add/remove.
** 006 CA 20231113 The 'execute' method must be annotated with LegacySignature Type.Execute, and also can be
** dropped if is a no-op.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.oo.net.http;
import com.goldencode.p2j.oo.core.*;
import com.goldencode.p2j.oo.json.objectmodel.JsonArray;
import com.goldencode.p2j.oo.json.objectmodel.JsonObject;
import com.goldencode.p2j.oo.json.objectmodel.ObjectModelParser;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.oo.logging.*;
import com.goldencode.p2j.security.ContextLocal;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.Action;
import com.goldencode.p2j.util.BlockManager.Condition;
import com.goldencode.p2j.util.InternalEntry.*;
import static com.goldencode.p2j.report.ReportConstants.*;
import static com.goldencode.p2j.util.BlockManager.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
/**
*
* Stores cookies temporarily and persistently.
*
*/
@LegacyResource(resource = "OpenEdge.Net.HTTP.CookieJar")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public class CookieJar extends BaseObject
implements ICookieJar, ISupportInitialize, ISupportLogging
{
private class CookieJarEntry
{
final object<? extends Cookie> cookie;
final String domain;
final String path;
final String name;
final datetimetz created = TypeFactory.datetimetz();
final datetimetz lastAccess = TypeFactory.datetimetz();
CookieJarEntry(object<? extends Cookie> cookie, String domain, String path)
{
this(cookie, domain, path, datetimetz.now(), TypeFactory.datetimetz());
}
CookieJarEntry(object<? extends Cookie> cookie, String domain, String path, datetimetz created, datetimetz lastAccess)
{
this.cookie = cookie;
this.name = cookie.ref().getName().getValue();
this.domain = domain;
this.path = TextOps.isEmpty(path) ? "/" : path;
this.created.assign(created);
this.lastAccess.assign(lastAccess);
}
}
private final static String TAG_COOKIES = "cookies";
private final static String TAG_COOKIE_NAME = "CookieName";
private final static String TAG_COOKIE_DOMAIN = "Domain";
private final static String TAG_COOKIE_PATH = "Path";
private final static String TAG_COOKIE_DATA = "Cookie";
private final static String TAG_COOKIE_CREATED = "CreatedAt";
private final static String TAG_COOKIE_ACCESS = "LastAccessedAt";
@LegacySignature(type = Type.PROPERTY, name = "Logger")
private final object<? extends IlogWriter> logger = TypeFactory.object(IlogWriter.class);
@LegacySignature(type = Type.PROPERTY, name = "CookieJarPath")
private character cookieJarPath = TypeFactory.character();
// persistent are stored as static for client session
private static ContextLocal<List<CookieJarEntry>> persistentCookies = new ContextLocal<List<CookieJarEntry>>()
{
protected List<CookieJarEntry> initialValue()
{
return new ArrayList<CookieJarEntry>();
}
};
// session cookies are stored only for the jar lifetime
private List<CookieJarEntry> sessionCookies = new ArrayList<CookieJarEntry>();
@LegacySignature(type = Type.EXECUTE)
public void __net_http_CookieJar_execute__()
{
onBlockLevel(Condition.ERROR, Action.THROW);
}
/**
* Get logger.
*
* @return logger.
*/
@LegacySignature(returns = "OBJECT", qualified = "OpenEdge.Logging.ILogWriter", type = Type.GETTER, name = "Logger")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
@Override
public object<? extends IlogWriter> getLogger()
{
return function(this, "Logger", object.class, new Block((Body) () -> {
returnNormal(logger);
}));
}
/**
* Destroy
*/
@LegacySignature(type = Type.METHOD, name = "Destroy")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
@Override
public void destroy()
{
internalProcedure(CookieJar.class, this, "Destroy", new Block((Body) () -> {
_persistCookies();
}));
}
/**
* Initialize
*/
@LegacySignature(type = Type.METHOD, name = "Initialize")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
@Override
public void initialize()
{
internalProcedure(CookieJar.class, this, "Initialize", new Block((Body) () -> {
// load persisted cookies, no-op if file not found
character jarStore = FileSystemOps.searchPath(cookieJarPath);
if (jarStore.isUnknown())
return;
object<ObjectModelParser> parser = ObjectOps.newInstance(ObjectModelParser.class);
object<JsonObject> data = ObjectOps.cast(parser.ref().parseFile(jarStore), JsonObject.class);
if (data.ref().has(new character(TAG_COOKIES)).booleanValue()) {
object<? extends JsonArray> cookies = data.ref().getJsonArray(new character(TAG_COOKIES));
for (int i = 0; i < cookies.ref().getLength().intValue(); i++)
{
object<? extends JsonObject> cookie = cookies.ref().getJsonObject(new integer(i + 1));
CookieJarEntry entry = new CookieJarEntry(Cookie.parse(cookie.ref().getCharacter(new character(TAG_COOKIE_DATA))),
cookie.ref().getCharacter(new character(TAG_COOKIE_DOMAIN)).getValue(),
cookie.ref().getCharacter(new character(TAG_COOKIE_PATH)).getValue(),
cookie.ref().getDatetimeTz(new character(TAG_COOKIE_CREATED)),
cookie.ref().getDatetimeTz(new character(TAG_COOKIE_ACCESS)));
_addCookie(entry, persistentCookies.get());
}
}
}));
}
/**
* Set logger.
*
* @param _logger The logger instance.
*/
@LegacySignature(type = Type.SETTER, name = "Logger", parameters =
{
@LegacyParameter(name = "logger", type = "OBJECT", mode = "INPUT", qualified = "OpenEdge.Logging.ILogWriter")
})
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
@Override
public void setLogger(object<? extends IlogWriter> _logger)
{
object<? extends IlogWriter> logger = TypeFactory.initInput(_logger);
internalProcedure(this, "Logger", new Block((Body) () -> {
this.logger.assign(logger);
}));
}
@LegacySignature(returns = "CHARACTER", type = Type.GETTER, name = "CookieJarPath")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public character getCookieJarPath()
{
return function(CookieJar.class, this, "CookieJarPath", character.class,
new Block((Body) () -> {
returnNormal(cookieJarPath);
}));
}
@LegacySignature(type = Type.SETTER, name = "CookieJarPath", parameters = {
@LegacyParameter(name = "var", type = "CHARACTER", mode = "INPUT") })
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void setCookieJarPath(final character _var)
{
character var = TypeFactory.initInput(_var);
internalProcedure(CookieJar.class, this, "CookieJarPath", new Block((Body) () -> {
cookieJarPath.assign(var);
}));
}
@LegacySignature(type = Type.CONSTRUCTOR)
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void __net_http_CookieJar_constructor__()
{
internalProcedure(CookieJar.class, this, "__net_http_CookieJar_constructor__",
new Block((Body) () -> {
__lang_BaseObject_constructor__();
cookieJarPath.assign(TextOps.substitute("&1cookies.json",
EnvironmentOps.getTempDirectory()));
logger.assign(ObjectOps.newInstance(VoidLogger.class));
}));
}
@LegacySignature(type = Type.DESTRUCTOR)
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void __net_http_CookieJar_destructor__()
{
internalProcedure(CookieJar.class, this, "__net_http_CookieJar_destructor__",
new Block((Body) () -> {
this.destroy();
}));
}
@LegacySignature(type = Type.METHOD, name = "AddCookie", parameters = {
@LegacyParameter(name = "pcDomain", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "pcPath", type = "CHARACTER", mode = "INPUT"),
@LegacyParameter(name = "poCookie", type = "OBJECT", qualified = "openedge.net.http.cookie", mode = "INPUT") })
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void addCookie(final character _pcDomain, final character _pcPath,
final object<? extends com.goldencode.p2j.oo.net.http.Cookie> _poCookie)
{
character pcDomain = TypeFactory.initInput(_pcDomain);
character pcPath = TypeFactory.initInput(_pcPath);
object<? extends com.goldencode.p2j.oo.net.http.Cookie> poCookie = TypeFactory
.initInput(_poCookie);
internalProcedure(CookieJar.class, this, "AddCookie", new Block((Body) () -> {
Assert.notNull(poCookie, new character("Cookie"));
// no error thrown if domain doesn't match
if (pcDomain.isUnknown()
|| pcDomain.getValue().endsWith(poCookie.ref().getDomain().getValue()))
{
Assert.notNullOrEmpty(pcDomain, new character("Cookie Domain"));
Assert.notNull(pcPath, new character("Cookie Path"));
CookieJarEntry entry = new CookieJarEntry(poCookie, pcDomain.getValue(),
pcPath.getValue());
// no expiration set, session cookie
if (poCookie.ref().getExpiresAt().isUnknown())
{
_addCookie(entry, sessionCookies);
}
else
{
// don't save it if expired already
if (!_isExpired(poCookie.ref()))
_addCookie(entry, persistentCookies.get());
}
}
else if (logger._isValid())
{
logger.ref().debug(TextOps.substitute("Bad domain &1 for &2", pcDomain, poCookie));
}
}));
}
@LegacySignature(type = Type.METHOD, name = "AddCookie", parameters = {
@LegacyParameter(name = "poCookie", type = "OBJECT", qualified = "openedge.net.http.cookie", mode = "INPUT") })
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void addCookie(final object<? extends com.goldencode.p2j.oo.net.http.Cookie> _poCookie)
{
object<? extends com.goldencode.p2j.oo.net.http.Cookie> poCookie = TypeFactory
.initInput(_poCookie);
internalProcedure(CookieJar.class, this, "AddCookie", new Block((Body) () -> {
Assert.notNull(poCookie, new character("Cookie"));
addCookie(poCookie.ref().getDomain(), poCookie.ref().getPath(), poCookie);
}));
}
@LegacySignature(type = Type.METHOD, name = "AddCookies", parameters = {
@LegacyParameter(name = "poCookies", type = "OBJECT", extent = -1, qualified = "openedge.net.http.cookie", mode = "INPUT") })
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void addCookies(
final object<? extends com.goldencode.p2j.oo.net.http.Cookie>[] _poCookies)
{
object<? extends com.goldencode.p2j.oo.net.http.Cookie>[] poCookies = TypeFactory
.initInput(_poCookies);
internalProcedure(CookieJar.class, this, "AddCookies", new Block((Body) () -> {
Assert.notNull(poCookies, new character("Cookies"));
for (int i = 0; i < poCookies.length; i++)
{
addCookie(poCookies[i]);
}
}));
}
@LegacySignature(type = Type.METHOD, name = "ClearPersistentCookies")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void clearPersistentCookies()
{
internalProcedure(CookieJar.class, this, "ClearPersistentCookies", new Block((Body) () -> {
persistentCookies.get().clear();
_persistCookies();
if (logger._isValid())
logger.ref().debug(TextOps.substitute("Cleared persistent cookies: &1", datetimetz.now()));
}));
}
@LegacySignature(type = Type.METHOD, name = "ClearSessionCookies")
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public void clearSessionCookies()
{
internalProcedure(CookieJar.class, this, "ClearSessionCookies", new Block((Body) () -> {
sessionCookies.clear();
if (logger._isValid())
logger.ref().debug(TextOps.substitute("Cleared session cookies: &1", datetimetz.now()));
}));
}
@LegacySignature(type = Type.CONSTRUCTOR)
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public static void __net_http_CookieJar_constructor__static__()
{
externalProcedure(CookieJar.class, new Block((Body) () -> {
onBlockLevel(Condition.ERROR, Action.THROW);
}));
}
@LegacySignature(returns = "INTEGER", type = Type.METHOD, name = "GetCookies", parameters = {
@LegacyParameter(name = "poUri", type = "OBJECT", qualified = "openedge.net.uri", mode = "INPUT"),
@LegacyParameter(name = "poCookies", type = "OBJECT", extent = -1, qualified = "openedge.net.http.cookie", mode = "OUTPUT") })
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public integer getCookies(final object<? extends com.goldencode.p2j.oo.net.Uri> _poUri,
final OutputExtentParameter<object<? extends com.goldencode.p2j.oo.net.http.Cookie>> _extpoCookies)
{
object<? extends com.goldencode.p2j.oo.net.Uri> poUri = TypeFactory.initInput(_poUri);
object[][] extpoCookies = { TypeFactory.initOutput(_extpoCookies) };
return function(CookieJar.class, this, "GetCookies", integer.class, new Block((Body) () -> {
String domain = poUri.ref().getHost().getValue().toLowerCase();
String path = poUri.ref().getPath().getValue().toLowerCase();
String scheme = poUri.ref().getScheme().getValue();
boolean secure = "https".equalsIgnoreCase(scheme);
boolean http = secure || "http".equalsIgnoreCase(scheme);
ArrayList<object<? extends Cookie>> all = new ArrayList<object<? extends Cookie>>();
// persistent first
all.addAll(_getCookies(persistentCookies.get(), domain, path, http, secure));
all.addAll(_getCookies(sessionCookies, domain, path, http, secure));
if (all.size() > 0) {
extpoCookies[0] = ArrayAssigner.resize(extpoCookies[0], all.size());
for (int i = 0; i < all.size(); i++)
{
extpoCookies[0][i].assign(all.get(i));
}
}
returnNormal(new integer(extpoCookies[0].length));
}));
}
@LegacySignature(returns = "LOGICAL", type = Type.METHOD, name = "RemoveCookie", parameters = {
@LegacyParameter(name = "poCookie", type = "OBJECT", qualified = "openedge.net.http.cookie", mode = "INPUT") })
@LegacyResourceSupport(supportLvl = CVT_LVL_FULL | RT_LVL_FULL)
public logical removeCookie(
final object<? extends com.goldencode.p2j.oo.net.http.Cookie> _poCookie)
{
object<? extends com.goldencode.p2j.oo.net.http.Cookie> poCookie = TypeFactory
.initInput(_poCookie);
return function(CookieJar.class, this, "RemoveCookie", logical.class,
new Block((Body) () -> {
Assert.notNull(poCookie, new character("Cookie"));
returnNormal(new logical(_removeCookie(poCookie.ref(), sessionCookies)
|| _removeCookie(poCookie.ref(), persistentCookies.get())));
}));
}
private boolean _removeCookie(Cookie cookie,
List<CookieJarEntry> store)
{
Optional<CookieJarEntry> first = store.stream()
.filter(e -> e.domain.equalsIgnoreCase(cookie.getDomain().getValue())
&& e.path.equalsIgnoreCase(cookie.getPath().getValue())
&& e.name.equalsIgnoreCase(cookie.getName().getValue()))
.findFirst();
if (first.isPresent()) {
CookieJarEntry entry = first.get();
if (store.remove(entry)) {
if (entry.cookie._isValid())
ObjectOps.decrement(entry.cookie.ref());
return true;
}
}
return false;
}
private void _addCookie(CookieJarEntry cookie, List<CookieJarEntry> store)
{
Optional<CookieJarEntry> find = store.stream()
.filter(e -> e.domain.equalsIgnoreCase(cookie.domain)
&& e.path.equalsIgnoreCase(cookie.path)
&& e.name.equalsIgnoreCase(cookie.name))
.findFirst();
if (find.isPresent())
{
// already have a cookie with same name for domain/path pair
CookieJarEntry entry = find.get();
if (entry.cookie._isValid())
ObjectOps.decrement(entry.cookie.ref());
entry.cookie.assign(cookie.cookie);
}
else
{
store.add(cookie);
}
ObjectOps.increment(cookie.cookie.ref());
}
private boolean _isExpired(Cookie cookie)
{
return !cookie.getExpiresAt().isUnknown()
&& DateOps.compare(cookie.getExpiresAt(), datetimetz.now()) < 0;
}
private object<JsonObject> _serializeCookie(CookieJarEntry poCookie)
{
object<JsonObject> jsonObj = ObjectOps.newInstance(JsonObject.class);
Cookie cookie = poCookie.cookie.ref();
jsonObj.ref().add(new character(TAG_COOKIE_NAME), new character(poCookie.name));
jsonObj.ref().add(new character(TAG_COOKIE_DOMAIN), new character(poCookie.domain));
jsonObj.ref().add(new character(TAG_COOKIE_PATH), new character(poCookie.path));
jsonObj.ref().add(new character(TAG_COOKIE_DATA), cookie.toLegacyString());
jsonObj.ref().add(new character(TAG_COOKIE_CREATED), poCookie.created);
jsonObj.ref().add(new character(TAG_COOKIE_ACCESS), poCookie.lastAccess);
return jsonObj;
}
private Collection<object<? extends Cookie>> _getCookies(List<CookieJarEntry> store,
String domain, String path, boolean http, boolean secure)
{
ArrayList<object<? extends Cookie>> cookies = new ArrayList<object<? extends Cookie>>();
for (Iterator iterator = store.iterator(); iterator.hasNext();)
{
CookieJarEntry cookieJarEntry = (CookieJarEntry) iterator.next();
// match domain and path
if (path.startsWith(cookieJarEntry.path.toLowerCase())
&& (cookieJarEntry.domain.equalsIgnoreCase(domain)
|| (cookieJarEntry.domain.indexOf('.') == 0
&& domain.endsWith(cookieJarEntry.domain.toLowerCase()))))
{
Cookie cookie = cookieJarEntry.cookie.ref();
// check expiration
if (_isExpired(cookie))
{
// already expired, no point in keeping it around
iterator.remove();
}
// check if http only/secure is set on cookie
else if ((!cookie.getHttpOnly().booleanValue() || http)
&& (!cookie.getSecure().booleanValue() || secure))
{
cookies.add(cookieJarEntry.cookie);
}
}
}
return cookies;
}
private void _persistCookies () {
final object<JsonObject> jsonObj = ObjectOps.newInstance(JsonObject.class);
final object<JsonArray> jsonArr = ObjectOps.newInstance(JsonArray.class);
persistentCookies.get().stream()
.filter(c -> c.cookie._isValid() && !_isExpired(c.cookie.ref()))
.forEach(c -> jsonArr.ref().add_2(_serializeCookie(c)));
jsonObj.ref().add(new character(TAG_COOKIES), jsonArr);
jsonObj.ref().writeFile(cookieJarPath);
}
}