diff -r 4285d13fd3dc Lib/http/cookiejar.py
--- a/Lib/http/cookiejar.py Sat Feb 23 15:44:46 2013 -0800
+++ b/Lib/http/cookiejar.py Wed Feb 27 07:28:31 2013 -0800
@@ -7,26 +7,16 @@
attributes of the HTTP cookie system as cookie-attributes, to distinguish
them clearly from Python attributes.
-Class diagram (note that BSDDBCookieJar and the MSIE* classes are not
-distributed with the Python standard library, but are available from
-http://wwwsearch.sf.net/):
- CookieJar____
- / \ \
- FileCookieJar \ \
- / | \ \ \
- MozillaCookieJar | LWPCookieJar \ \
- | | \
- | ---MSIEBase | \
- | / | | \
- | / MSIEDBCookieJar BSDDBCookieJar
- |/
- MSIECookieJar
-
+ FileCookieProcessor CookieJar
+ / \
+ MozillaCookieProcessor LWPCookieProcessor
+
"""
__all__ = ['Cookie', 'CookieJar', 'CookiePolicy', 'DefaultCookiePolicy',
- 'FileCookieJar', 'LWPCookieJar', 'LoadError', 'MozillaCookieJar']
+ 'FileCookieProcessor', 'LWPCookieProcessor', 'LoadError',
+ 'MozillaCookieProcessor']
import copy
import datetime
@@ -38,6 +28,7 @@
except ImportError:
import dummy_threading as _threading
import http.client # only for the default HTTP port
+from abc import ABCMeta, abstractmethod
from calendar import timegm
debug = False # set to True to enable debugging via the logging module
@@ -1733,62 +1724,64 @@
# derives from OSError for backwards-compatibility with Python 2.4.0
class LoadError(OSError): pass
-class FileCookieJar(CookieJar):
- """CookieJar that can be loaded from and saved to a file."""
+class FileCookieProcessor(metaclass=ABCMeta):
+ """Abstract Base Class for file-based cookie processor
+ """
- def __init__(self, filename=None, delayload=False, policy=None):
+ def __init__(self, filename=None, cookiejar=None):
"""
Cookies are NOT loaded from the named file until either the .load() or
.revert() method is called.
"""
- CookieJar.__init__(self, policy)
+ if cookiejar is None:
+ cookiejar = CookieJar()
+ self.cookiejar = cookiejar
+ self.filename = filename
+
+ @abstractmethod
+ def save(self, filename=None, ignore_discard=False, ignore_expires=False):
+ """Save cookies to a file.
+
+ Concrete implementations should call FileCookieJar.load first in order
+ to ensure self.filename is set correctly.
+ """
if filename is not None:
- try:
- filename+""
- except:
- raise ValueError("filename must be string-like")
- self.filename = filename
- self.delayload = bool(delayload)
+ self.filename = filename
- def save(self, filename=None, ignore_discard=False, ignore_expires=False):
- """Save cookies to a file."""
- raise NotImplementedError()
+ @abstractmethod
+ def load(self, filename, ignore_discard=False, ignore_expires=False):
+ """Load cookies from a file.
+
+ Concrete implementations should call FileCookieJar.load first in order
+ to ensure self.filename is set correctly.
+ """
+ if filename is not None:
+ self.filename = filename
- def load(self, filename=None, ignore_discard=False, ignore_expires=False):
- """Load cookies from a file."""
- if filename is None:
- if self.filename is not None: filename = self.filename
- else: raise ValueError(MISSING_FILENAME_TEXT)
-
- with open(filename) as f:
- self._really_load(f, filename, ignore_discard, ignore_expires)
-
- def revert(self, filename=None,
- ignore_discard=False, ignore_expires=False):
+ def revert(self, filename=None, ignore_discard=False,
+ ignore_expires=False):
"""Clear all cookies and reload cookies from a saved file.
Raises LoadError (or OSError) if reversion is not successful; the
object's state will not be altered if this happens.
"""
- if filename is None:
- if self.filename is not None: filename = self.filename
- else: raise ValueError(MISSING_FILENAME_TEXT)
+ if filename is not None:
+ self.filename = filename
- self._cookies_lock.acquire()
+ self.cookiejar._cookies_lock.acquire()
try:
-
- old_state = copy.deepcopy(self._cookies)
- self._cookies = {}
+ old_state = copy.deepcopy(self.cookiejar._cookies)
+ self.cookiejar._cookies = {}
try:
self.load(filename, ignore_discard, ignore_expires)
except OSError:
- self._cookies = old_state
+ self.cookiejar._cookies = old_state
raise
finally:
- self._cookies_lock.release()
+ self.cookiejar._cookies_lock.release()
def lwp_cookie_str(cookie):
@@ -1819,7 +1812,7 @@
return join_header_words([h])
-class LWPCookieJar(FileCookieJar):
+class LWPCookieProcessor(FileCookieProcessor):
"""
The LWPCookieJar saves a sequence of "Set-Cookie3" lines.
"Set-Cookie3" is the format used by the libwww-perl libary, not known
@@ -1831,6 +1824,9 @@
as_lwp_str(ignore_discard=True, ignore_expired=True)
"""
+ def __init__(self, filename=None, cookiejar=None):
+ FileCookieProcessor.__init__(self, cookiejar=cookiejar)
+ self.filename = filename
def as_lwp_str(self, ignore_discard=True, ignore_expires=True):
"""Return cookies as a string of "\\n"-separated "Set-Cookie3" headers.
@@ -1840,7 +1836,7 @@
"""
now = time.time()
r = []
- for cookie in self:
+ for cookie in self.cookiejar:
if not ignore_discard and cookie.discard:
continue
if not ignore_expires and cookie.is_expired(now):
@@ -1849,97 +1845,100 @@
return "\n".join(r+[""])
def save(self, filename=None, ignore_discard=False, ignore_expires=False):
- if filename is None:
- if self.filename is not None: filename = self.filename
- else: raise ValueError(MISSING_FILENAME_TEXT)
+ if filename is not None:
+ self.filename = filename
- with open(filename, "w") as f:
+ with open(self.filename, "w") as f:
# There really isn't an LWP Cookies 2.0 format, but this indicates
# that there is extra information in here (domain_dot and
# port_spec) while still being compatible with libwww-perl, I hope.
f.write("#LWP-Cookies-2.0\n")
f.write(self.as_lwp_str(ignore_discard, ignore_expires))
- def _really_load(self, f, filename, ignore_discard, ignore_expires):
- magic = f.readline()
- if not self.magic_re.search(magic):
- msg = ("%r does not look like a Set-Cookie3 (LWP) format "
- "file" % filename)
- raise LoadError(msg)
+ def load(self, filename=None, ignore_discard=False, ignore_expires=None):
+ FileCookieProcessor.load(self, filename=filename,
+ ignore_discard=ignore_discard, ignore_expires=ignore_expires)
- now = time.time()
+ with open(self.filename) as f:
+ magic = f.readline()
+ if not self.cookiejar.magic_re.search(magic):
+ msg = ("%r does not look like a Set-Cookie3 (LWP) format "
+ "file" % filename)
+ raise LoadError(msg)
- header = "Set-Cookie3:"
- boolean_attrs = ("port_spec", "path_spec", "domain_dot",
- "secure", "discard")
- value_attrs = ("version",
- "port", "path", "domain",
- "expires",
- "comment", "commenturl")
+ now = time.time()
- try:
- while 1:
- line = f.readline()
- if line == "": break
- if not line.startswith(header):
- continue
- line = line[len(header):].strip()
+ header = "Set-Cookie3:"
+ boolean_attrs = ("port_spec", "path_spec", "domain_dot",
+ "secure", "discard")
+ value_attrs = ("version",
+ "port", "path", "domain",
+ "expires",
+ "comment", "commenturl")
- for data in split_header_words([line]):
- name, value = data[0]
- standard = {}
- rest = {}
- for k in boolean_attrs:
- standard[k] = False
- for k, v in data[1:]:
- if k is not None:
- lc = k.lower()
- else:
- lc = None
- # don't lose case distinction for unknown fields
- if (lc in value_attrs) or (lc in boolean_attrs):
- k = lc
- if k in boolean_attrs:
- if v is None: v = True
- standard[k] = v
- elif k in value_attrs:
- standard[k] = v
- else:
- rest[k] = v
+ try:
+ while 1:
+ line = f.readline()
+ if line == "": break
+ if not line.startswith(header):
+ continue
+ line = line[len(header):].strip()
- h = standard.get
- expires = h("expires")
- discard = h("discard")
- if expires is not None:
- expires = iso2time(expires)
- if expires is None:
- discard = True
- domain = h("domain")
- domain_specified = domain.startswith(".")
- c = Cookie(h("version"), name, value,
- h("port"), h("port_spec"),
- domain, domain_specified, h("domain_dot"),
- h("path"), h("path_spec"),
- h("secure"),
- expires,
- discard,
- h("comment"),
- h("commenturl"),
- rest)
- if not ignore_discard and c.discard:
- continue
- if not ignore_expires and c.is_expired(now):
- continue
- self.set_cookie(c)
- except OSError:
- raise
- except Exception:
- _warn_unhandled_exception()
- raise LoadError("invalid Set-Cookie3 format file %r: %r" %
- (filename, line))
+ for data in split_header_words([line]):
+ name, value = data[0]
+ standard = {}
+ rest = {}
+ for k in boolean_attrs:
+ standard[k] = False
+ for k, v in data[1:]:
+ if k is not None:
+ lc = k.lower()
+ else:
+ lc = None
+ # don't lose case distinction for unknown fields
+ if (lc in value_attrs) or (lc in boolean_attrs):
+ k = lc
+ if k in boolean_attrs:
+ if v is None: v = True
+ standard[k] = v
+ elif k in value_attrs:
+ standard[k] = v
+ else:
+ rest[k] = v
+ h = standard.get
+ expires = h("expires")
+ discard = h("discard")
+ if expires is not None:
+ expires = iso2time(expires)
+ if expires is None:
+ discard = True
+ domain = h("domain")
+ domain_specified = domain.startswith(".")
+ c = Cookie(h("version"), name, value,
+ h("port"), h("port_spec"),
+ domain, domain_specified, h("domain_dot"),
+ h("path"), h("path_spec"),
+ h("secure"),
+ expires,
+ discard,
+ h("comment"),
+ h("commenturl"),
+ rest)
+ if not ignore_discard and c.discard:
+ continue
+ if not ignore_expires and c.is_expired(now):
+ continue
+ self.cookiejar.set_cookie(c)
+ except OSError:
+ raise
+ except Exception:
+ _warn_unhandled_exception()
+ raise LoadError("invalid Set-Cookie3 format file %r: %r" %
+ (filename, line))
-class MozillaCookieJar(FileCookieJar):
+
+class MozillaCookieProcessor(FileCookieProcessor):
"""
WARNING: you may want to backup your browser's cookies file if you use
@@ -1977,72 +1976,77 @@
# This is a generated file! Do not edit.
"""
+ def __init__(self, filename=None, cookiejar=None):
+ FileCookieProcessor.__init__(self, filename=filename,
+ cookiejar=cookiejar)
- def _really_load(self, f, filename, ignore_discard, ignore_expires):
+ def load(self, filename=None, ignore_discard=False, ignore_expires=False):
+ FileCookieProcessor.load(self, filename=filename,
+ ignore_discard=ignore_discard, ignore_expires=ignore_expires)
now = time.time()
- magic = f.readline()
- if not self.magic_re.search(magic):
- f.close()
- raise LoadError(
- "%r does not look like a Netscape format cookies file" %
- filename)
+ with open(self.filename) as f:
+ magic = f.readline()
+ if not self.magic_re.search(magic):
+ raise LoadError(
+ "%r does not look like a Netscape format cookies file" %
+ filename)
- try:
- while 1:
- line = f.readline()
- if line == "": break
+ try:
+ while 1:
+ line = f.readline()
+ if line == "": break
- # last field may be absent, so keep any trailing tab
- if line.endswith("\n"): line = line[:-1]
+ # last field may be absent, so keep any trailing tab
+ if line.endswith("\n"): line = line[:-1]
- # skip comments and blank lines XXX what is $ for?
- if (line.strip().startswith(("#", "$")) or
- line.strip() == ""):
- continue
+ # skip comments and blank lines XXX what is $ for?
+ if (line.strip().startswith(("#", "$")) or
+ line.strip() == ""):
+ continue
- domain, domain_specified, path, secure, expires, name, value = \
- line.split("\t")
- secure = (secure == "TRUE")
- domain_specified = (domain_specified == "TRUE")
- if name == "":
- # cookies.txt regards 'Set-Cookie: foo' as a cookie
- # with no name, whereas http.cookiejar regards it as a
- # cookie with no value.
- name = value
- value = None
+ domain, domain_specified, path, secure, expires, name, \
+ value = line.split("\t")
+ secure = (secure == "TRUE")
+ domain_specified = (domain_specified == "TRUE")
+ if name == "":
+ # cookies.txt regards 'Set-Cookie: foo' as a cookie
+ # with no name, whereas http.cookiejar regards it as a
+ # cookie with no value.
+ name = value
+ value = None
- initial_dot = domain.startswith(".")
- assert domain_specified == initial_dot
+ initial_dot = domain.startswith(".")
+ assert domain_specified == initial_dot
- discard = False
- if expires == "":
- expires = None
- discard = True
+ discard = False
+ if expires == "":
+ expires = None
+ discard = True
- # assume path_specified is false
- c = Cookie(0, name, value,
- None, False,
- domain, domain_specified, initial_dot,
- path, False,
- secure,
- expires,
- discard,
- None,
- None,
- {})
- if not ignore_discard and c.discard:
- continue
- if not ignore_expires and c.is_expired(now):
- continue
- self.set_cookie(c)
+ # assume path_specified is false
+ c = Cookie(0, name, value,
+ None, False,
+ domain, domain_specified, initial_dot,
+ path, False,
+ secure,
+ expires,
+ discard,
+ None,
+ None,
+ {})
+ if not ignore_discard and c.discard:
+ continue
+ if not ignore_expires and c.is_expired(now):
+ continue
+ self.cookiejar.set_cookie(c)
- except OSError:
- raise
- except Exception:
- _warn_unhandled_exception()
- raise LoadError("invalid Netscape format cookies file %r: %r" %
- (filename, line))
+ except OSError:
+ raise
+ except Exception:
+ _warn_unhandled_exception()
+ raise LoadError("invalid Netscape format cookies file %r: %r" %
+ (filename, line))
def save(self, filename=None, ignore_discard=False, ignore_expires=False):
if filename is None:
@@ -2052,7 +2056,7 @@
with open(filename, "w") as f:
f.write(self.header)
now = time.time()
- for cookie in self:
+ for cookie in self.cookiejar:
if not ignore_discard and cookie.discard:
continue
if not ignore_expires and cookie.is_expired(now):
diff -r 4285d13fd3dc Lib/test/test_http_cookiejar.py
--- a/Lib/test/test_http_cookiejar.py Sat Feb 23 15:44:46 2013 -0800
+++ b/Lib/test/test_http_cookiejar.py Wed Feb 27 07:28:31 2013 -0800
@@ -9,10 +9,11 @@
from http.cookiejar import (time2isoz, http2time, time2netscape,
parse_ns_headers, join_header_words, split_header_words, Cookie,
- CookieJar, DefaultCookiePolicy, LWPCookieJar, MozillaCookieJar,
- LoadError, lwp_cookie_str, DEFAULT_HTTP_PORT, escape_path,
- reach, is_HDN, domain_match, user_domain_match, request_path,
- request_port, request_host)
+ CookieJar, DefaultCookiePolicy, LWPCookieProcessor,
+ MozillaCookieProcessor, LoadError, lwp_cookie_str,
+ DEFAULT_HTTP_PORT, escape_path, reach, is_HDN, domain_match,
+ user_domain_match, request_path, request_port, request_host,
+ FileCookieProcessor)
class DateTimeTests(unittest.TestCase):
@@ -235,23 +236,25 @@
def test_lwp_valueless_cookie(self):
# cookies with no value should be saved and loaded consistently
filename = test.support.TESTFN
- c = LWPCookieJar()
- interact_netscape(c, "http://www.acme.com/", 'boo')
- self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None)
+ proc = LWPCookieProcessor()
+ interact_netscape(proc.cookiejar, "http://www.acme.com/", 'boo')
+ self.assertEqual(proc.cookiejar._cookies["www.acme.com"]["/"][
+ "boo"].value, None)
try:
- c.save(filename, ignore_discard=True)
- c = LWPCookieJar()
- c.load(filename, ignore_discard=True)
+ proc.save(filename, ignore_discard=True)
+ proc = LWPCookieProcessor()
+ proc.load(filename, ignore_discard=True)
finally:
try: os.unlink(filename)
except OSError: pass
- self.assertEqual(c._cookies["www.acme.com"]["/"]["boo"].value, None)
+ self.assertEqual(proc.cookiejar._cookies["www.acme.com"]["/"][
+ "boo"].value, None)
def test_bad_magic(self):
# OSErrors (eg. file doesn't exist) are allowed to propagate
filename = test.support.TESTFN
- for cookiejar_class in LWPCookieJar, MozillaCookieJar:
- c = cookiejar_class()
+ for cookie_proc in LWPCookieProcessor, MozillaCookieProcessor:
+ c = cookie_proc()
try:
c.load(filename="for this test to work, a file with this "
"filename should not exist")
@@ -266,13 +269,31 @@
try:
with open(filename, "w") as f:
f.write("oops\n")
- for cookiejar_class in LWPCookieJar, MozillaCookieJar:
- c = cookiejar_class()
+ for cookie_proc in LWPCookieProcessor, MozillaCookieProcessor:
+ c = cookie_proc()
self.assertRaises(LoadError, c.load, filename)
finally:
try: os.unlink(filename)
except OSError: pass
+ def test_invalid_interface(self):
+ # ensure that instantiation of an invalid implementation will raise a
+ # TypeError
+ class InvalidFileCookieJar(FileCookieProcessor):
+ pass
+
+ try:
+ InvalidFileCookieJar()
+ except TypeError:
+ pass
+ except Exception as e:
+ self.fail('Unexpected exception {}'.format(e))
+
+ def test_concrete_lookup(self):
+ # expected implementations of FileCookieJar
+ self.assertEqual(set(FileCookieProcessor.__subclasses__()),
+ set([LWPCookieProcessor, MozillaCookieProcessor]))
+
class CookieTests(unittest.TestCase):
# XXX
# Get rid of string comparisons where not actually testing str / repr.
@@ -354,33 +375,34 @@
# missing = sign in Cookie: header is regarded by Mozilla as a missing
# name, and by http.cookiejar as a missing value
filename = test.support.TESTFN
- c = MozillaCookieJar(filename)
- interact_netscape(c, "http://www.acme.com/", 'eggs')
- interact_netscape(c, "http://www.acme.com/", '"spam"; path=/foo/')
- cookie = c._cookies["www.acme.com"]["/"]["eggs"]
+ proc = MozillaCookieProcessor(filename)
+ interact_netscape(proc.cookiejar, "http://www.acme.com/", 'eggs')
+ interact_netscape(proc.cookiejar, "http://www.acme.com/",
+ '"spam"; path=/foo/')
+ cookie = proc.cookiejar._cookies["www.acme.com"]["/"]["eggs"]
self.assertTrue(cookie.value is None)
self.assertEqual(cookie.name, "eggs")
- cookie = c._cookies["www.acme.com"]['/foo/']['"spam"']
+ cookie = proc.cookiejar._cookies["www.acme.com"]['/foo/']['"spam"']
self.assertTrue(cookie.value is None)
self.assertEqual(cookie.name, '"spam"')
self.assertEqual(lwp_cookie_str(cookie), (
r'"spam"; path="/foo/"; domain="www.acme.com"; '
'path_spec; discard; version=0'))
- old_str = repr(c)
- c.save(ignore_expires=True, ignore_discard=True)
+ old_str = repr(proc.cookiejar)
+ proc.save(ignore_expires=True, ignore_discard=True)
try:
- c = MozillaCookieJar(filename)
- c.revert(ignore_expires=True, ignore_discard=True)
+ proc = MozillaCookieProcessor(filename)
+ proc.revert(ignore_expires=True, ignore_discard=True)
finally:
- os.unlink(c.filename)
+ os.unlink(proc.filename)
# cookies unchanged apart from lost info re. whether path was specified
self.assertEqual(
- repr(c),
+ repr(proc.cookiejar),
re.sub("path_specified=%s" % True, "path_specified=%s" % False,
old_str)
)
- self.assertEqual(interact_netscape(c, "http://www.acme.com/foo/"),
- '"spam"; eggs')
+ self.assertEqual(interact_netscape(proc.cookiejar,
+ "http://www.acme.com/foo/"), '"spam"; eggs')
def test_rfc2109_handling(self):
# RFC 2109 cookies are handled as RFC 2965 or Netscape cookies,
@@ -1375,66 +1397,66 @@
# Test rejection of Set-Cookie2 responses based on domain, path, port.
pol = DefaultCookiePolicy(rfc2965=True)
- c = LWPCookieJar(policy=pol)
+ proc = LWPCookieProcessor(cookiejar=CookieJar(policy=pol))
max_age = "max-age=3600"
# illegal domain (no embedded dots)
- cookie = interact_2965(c, "http://www.acme.com",
+ cookie = interact_2965(proc.cookiejar, "http://www.acme.com",
'foo=bar; domain=".com"; version=1')
- self.assertTrue(not c)
+ self.assertTrue(not proc.cookiejar)
# legal domain
- cookie = interact_2965(c, "http://www.acme.com",
+ cookie = interact_2965(proc.cookiejar, "http://www.acme.com",
'ping=pong; domain="acme.com"; version=1')
- self.assertEqual(len(c), 1)
+ self.assertEqual(len(proc.cookiejar), 1)
# illegal domain (host prefix "www.a" contains a dot)
- cookie = interact_2965(c, "http://www.a.acme.com",
+ cookie = interact_2965(proc.cookiejar, "http://www.a.acme.com",
'whiz=bang; domain="acme.com"; version=1')
- self.assertEqual(len(c), 1)
+ self.assertEqual(len(proc.cookiejar), 1)
# legal domain
- cookie = interact_2965(c, "http://www.a.acme.com",
+ cookie = interact_2965(proc.cookiejar, "http://www.a.acme.com",
'wow=flutter; domain=".a.acme.com"; version=1')
- self.assertEqual(len(c), 2)
+ self.assertEqual(len(proc.cookiejar), 2)
# can't partially match an IP-address
- cookie = interact_2965(c, "http://125.125.125.125",
+ cookie = interact_2965(proc.cookiejar, "http://125.125.125.125",
'zzzz=ping; domain="125.125.125"; version=1')
- self.assertEqual(len(c), 2)
+ self.assertEqual(len(proc.cookiejar), 2)
# illegal path (must be prefix of request path)
- cookie = interact_2965(c, "http://www.sol.no",
+ cookie = interact_2965(proc.cookiejar, "http://www.sol.no",
'blah=rhubarb; domain=".sol.no"; path="/foo"; '
'version=1')
- self.assertEqual(len(c), 2)
+ self.assertEqual(len(proc.cookiejar), 2)
# legal path
- cookie = interact_2965(c, "http://www.sol.no/foo/bar",
+ cookie = interact_2965(proc.cookiejar, "http://www.sol.no/foo/bar",
'bing=bong; domain=".sol.no"; path="/foo"; '
'version=1')
- self.assertEqual(len(c), 3)
+ self.assertEqual(len(proc.cookiejar), 3)
# illegal port (request-port not in list)
- cookie = interact_2965(c, "http://www.sol.no",
+ cookie = interact_2965(proc.cookiejar, "http://www.sol.no",
'whiz=ffft; domain=".sol.no"; port="90,100"; '
'version=1')
- self.assertEqual(len(c), 3)
+ self.assertEqual(len(proc.cookiejar), 3)
# legal port
cookie = interact_2965(
- c, "http://www.sol.no",
+ proc.cookiejar, "http://www.sol.no",
r'bang=wallop; version=1; domain=".sol.no"; '
r'port="90,100, 80,8080"; '
r'max-age=100; Comment = "Just kidding! (\"|\\\\) "')
- self.assertEqual(len(c), 4)
+ self.assertEqual(len(proc.cookiejar), 4)
# port attribute without any value (current port)
- cookie = interact_2965(c, "http://www.sol.no",
+ cookie = interact_2965(proc.cookiejar, "http://www.sol.no",
'foo9=bar; version=1; domain=".sol.no"; port; '
'max-age=100;')
- self.assertEqual(len(c), 5)
+ self.assertEqual(len(proc.cookiejar), 5)
# encoded path
# LWP has this test, but unescaping allowed path characters seems
@@ -1443,24 +1465,24 @@
## r'foo8=bar; version=1; path="/%66oo"')
# but this is OK, because '<' is not an allowed HTTP URL path
# character:
- cookie = interact_2965(c, "http://www.sol.no/