Skip to content

Commit e638610

Browse files
kblokCopilot
andauthored
Bidi: Unlock cookies support (#2974)
* Bidi: Unlock cookies support * bring latest webdriver bidi package version * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * cr * fix build --------- Co-authored-by: Copilot <[email protected]>
1 parent b8bec2b commit e638610

File tree

6 files changed

+404
-4
lines changed

6 files changed

+404
-4
lines changed

lib/.claude/notify.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
# Default values for the notification
4+
TITLE="${1:-Claude Code}"
5+
MESSAGE="${2:-Task successfully completed in repo.}"
6+
7+
# Check for OS and use the appropriate command - Windows excluded
8+
if [[ "$OSTYPE" == "darwin"* ]]; then
9+
# macOS: Use osascript for a native notification
10+
osascript -e "display notification \"$MESSAGE\" with title \"$TITLE\" sound name \"Glass\""
11+
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
12+
# Linux (requires 'notify-send', usually pre-installed on desktop environments)
13+
notify-send "$TITLE" "$MESSAGE"
14+
fi

lib/.claude/settings.local.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(grep:*)",
5+
"Bash(dotnet test:*)",
6+
"Bash(dotnet build:*)",
7+
"Bash(dotnet clean:*)",
8+
"Bash(find:*)",
9+
"Read(//Users/dario/Code/hardkoded/puppeteer-sharp/**)",
10+
"Read(//Users/dario/Code/puppeteer/puppeteer/packages/puppeteer-core/src/bidi/**)",
11+
"Read(//Users/dario/Code/puppeteer/puppeteer/**)",
12+
"Read(//Users/dario/Code/webdriverbidi-net/**)",
13+
"Bash(mkdir -p /Users/dario/Code/hardkoded/puppeteer-sharp/lib/PuppeteerSharp.Tests/InterventionHeadersTests)",
14+
"Bash(mkdir -p /Users/dario/Code/hardkoded/puppeteer-sharp/lib/PuppeteerSharp.Tests/ConnectTests)",
15+
"Bash(rm /Users/dario/Code/hardkoded/puppeteer-sharp/lib/PuppeteerSharp.Tests/ChromiumOnlyTests/PageTests.cs)",
16+
"Bash(rm /Users/dario/Code/hardkoded/puppeteer-sharp/lib/PuppeteerSharp.Tests/ChromiumOnlyTests/BrowserUrlOptionTests.cs)",
17+
"Bash(rmdir /Users/dario/Code/hardkoded/puppeteer-sharp/lib/PuppeteerSharp.Tests/ChromiumOnlyTests)",
18+
"Bash(git checkout Storage/DeleteCookiesCommandResult.cs Storage/GetCookiesCommandResult.cs Storage/SetCookieCommandResult.cs)",
19+
"Bash(dotnet package search WebDriverBiDi --exact-match)",
20+
"Bash(pkill -f \"dotnet test\")"
21+
],
22+
"deny": []
23+
},
24+
"hooks": {
25+
"Stop": [
26+
{
27+
"hooks": [
28+
{
29+
"type": "command",
30+
"command": "$CLAUDE_PROJECT_DIR/.claude/notify.sh \"Claude Code\" \"Task Completed!\""
31+
}
32+
]
33+
}
34+
]
35+
}
36+
}

lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,5 +1769,20 @@
17691769
"expectations": [
17701770
"FAIL"
17711771
]
1772+
},
1773+
{
1774+
"comment": "BidiBrowserContext.CloseAsync() is not implemented - fails with NotImplementedException",
1775+
"testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts",
1776+
"platforms": [
1777+
"darwin",
1778+
"linux",
1779+
"win32"
1780+
],
1781+
"parameters": [
1782+
"webDriverBiDi"
1783+
],
1784+
"expectations": [
1785+
"SKIP"
1786+
]
17721787
}
17731788
]
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// * MIT License
2+
// *
3+
// * Copyright (c) Darío Kondratiuk
4+
// *
5+
// * Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// * of this software and associated documentation files (the "Software"), to deal
7+
// * in the Software without restriction, including without limitation the rights
8+
// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// * copies of the Software, and to permit persons to whom the Software is
10+
// * furnished to do so, subject to the following conditions:
11+
// *
12+
// * The above copyright notice and this permission notice shall be included in all
13+
// * copies or substantial portions of the Software.
14+
// *
15+
// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// * SOFTWARE.
22+
23+
using System;
24+
using WebDriverBiDi.Network;
25+
using WebDriverBiDi.Storage;
26+
using BidiCookie = WebDriverBiDi.Network.Cookie;
27+
28+
namespace PuppeteerSharp.Bidi;
29+
30+
internal static class BidiCookieHelper
31+
{
32+
/// <summary>
33+
/// Converts a BiDi cookie to a PuppeteerSharp cookie.
34+
/// </summary>
35+
public static CookieParam BidiToPuppeteerCookie(BidiCookie bidiCookie)
36+
=> new()
37+
{
38+
Name = bidiCookie.Name,
39+
Value = bidiCookie.Value.Value,
40+
Domain = bidiCookie.Domain,
41+
Path = bidiCookie.Path,
42+
Size = (int)bidiCookie.Size,
43+
HttpOnly = bidiCookie.HttpOnly,
44+
Secure = bidiCookie.Secure,
45+
SameSite = ConvertSameSiteBidiToPuppeteer(bidiCookie.SameSite),
46+
Expires = bidiCookie.EpochExpires.HasValue ? (double)bidiCookie.EpochExpires.Value / 1000 : -1,
47+
Session = !bidiCookie.EpochExpires.HasValue || bidiCookie.EpochExpires.Value == 0,
48+
};
49+
50+
/// <summary>
51+
/// Converts a PuppeteerSharp cookie to a BiDi partial cookie.
52+
/// </summary>
53+
public static PartialCookie PuppeteerToBidiCookie(CookieParam cookie, string domain)
54+
{
55+
var bidiCookie = new PartialCookie(
56+
cookie.Name,
57+
BytesValue.FromString(cookie.Value),
58+
domain)
59+
{
60+
Path = cookie.Path,
61+
SameSite = ConvertSameSitePuppeteerToBidi(cookie.SameSite),
62+
};
63+
64+
// Only set HttpOnly and Secure if explicitly provided
65+
if (cookie.HttpOnly.HasValue)
66+
{
67+
bidiCookie.HttpOnly = cookie.HttpOnly.Value;
68+
}
69+
70+
if (cookie.Secure.HasValue)
71+
{
72+
bidiCookie.Secure = cookie.Secure.Value;
73+
}
74+
75+
// Convert expiration
76+
if (cookie.Expires.HasValue && cookie.Expires.Value != -1)
77+
{
78+
bidiCookie.Expires = DateTimeOffset.FromUnixTimeSeconds((long)cookie.Expires.Value).DateTime;
79+
}
80+
81+
// Add CDP-specific properties if needed
82+
if (cookie.SameParty.HasValue)
83+
{
84+
bidiCookie.AdditionalData["goog:sameParty"] = cookie.SameParty.Value;
85+
}
86+
87+
if (cookie.SourceScheme.HasValue)
88+
{
89+
bidiCookie.AdditionalData["goog:sourceScheme"] = ConvertSourceSchemeEnumToString(cookie.SourceScheme.Value);
90+
}
91+
92+
if (cookie.Priority.HasValue)
93+
{
94+
bidiCookie.AdditionalData["goog:priority"] = ConvertPriorityEnumToString(cookie.Priority.Value);
95+
}
96+
97+
if (!string.IsNullOrEmpty(cookie.Url))
98+
{
99+
bidiCookie.AdditionalData["goog:url"] = cookie.Url;
100+
}
101+
102+
return bidiCookie;
103+
}
104+
105+
/// <summary>
106+
/// Checks if a cookie matches a URL according to the spec.
107+
/// </summary>
108+
public static bool TestUrlMatchCookie(CookieParam cookie, Uri url)
109+
{
110+
return TestUrlMatchCookieHostname(cookie, url) && TestUrlMatchCookiePath(cookie, url);
111+
}
112+
113+
/// <summary>
114+
/// Checks if cookie domain matches URL hostname.
115+
/// </summary>
116+
private static bool TestUrlMatchCookieHostname(CookieParam cookie, Uri url)
117+
{
118+
var cookieDomain = cookie.Domain?.ToLowerInvariant() ?? string.Empty;
119+
var urlHostname = url.Host.ToLowerInvariant();
120+
121+
if (cookieDomain == urlHostname)
122+
{
123+
return true;
124+
}
125+
126+
// TODO: does not consider additional restrictions w.r.t to IP
127+
// addresses which is fine as it is for representation and does not
128+
// mean that cookies actually apply that way in the browser.
129+
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
130+
return cookieDomain.StartsWith(".", StringComparison.Ordinal) && urlHostname.EndsWith(cookieDomain, StringComparison.Ordinal);
131+
}
132+
133+
/// <summary>
134+
/// Checks if cookie path matches URL path.
135+
/// Spec: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4.
136+
/// </summary>
137+
private static bool TestUrlMatchCookiePath(CookieParam cookie, Uri url)
138+
{
139+
var uriPath = url.AbsolutePath;
140+
var cookiePath = cookie.Path;
141+
142+
if (uriPath == cookiePath)
143+
{
144+
// The cookie-path and the request-path are identical.
145+
return true;
146+
}
147+
148+
if (uriPath.StartsWith(cookiePath, StringComparison.Ordinal))
149+
{
150+
// The cookie-path is a prefix of the request-path.
151+
if (cookiePath.EndsWith("/", StringComparison.Ordinal))
152+
{
153+
// The last character of the cookie-path is %x2F ("/").
154+
return true;
155+
}
156+
157+
if (uriPath.Length > cookiePath.Length && uriPath[cookiePath.Length] == '/')
158+
{
159+
// The first character of the request-path that is not included in the cookie-path
160+
// is a %x2F ("/") character.
161+
return true;
162+
}
163+
}
164+
165+
return false;
166+
}
167+
168+
private static SameSite ConvertSameSiteBidiToPuppeteer(CookieSameSiteValue sameSite)
169+
{
170+
return sameSite switch
171+
{
172+
CookieSameSiteValue.Strict => SameSite.Strict,
173+
CookieSameSiteValue.Lax => SameSite.Lax,
174+
_ => SameSite.None,
175+
};
176+
}
177+
178+
private static CookieSameSiteValue? ConvertSameSitePuppeteerToBidi(SameSite? sameSite)
179+
{
180+
return sameSite switch
181+
{
182+
SameSite.Strict => CookieSameSiteValue.Strict,
183+
SameSite.Lax => CookieSameSiteValue.Lax,
184+
SameSite.None => CookieSameSiteValue.None,
185+
_ => null,
186+
};
187+
}
188+
189+
private static string ConvertSourceSchemeEnumToString(CookieSourceScheme sourceScheme)
190+
{
191+
return sourceScheme switch
192+
{
193+
CookieSourceScheme.Unset => "Unset",
194+
CookieSourceScheme.NonSecure => "NonSecure",
195+
CookieSourceScheme.Secure => "Secure",
196+
_ => "Unset",
197+
};
198+
}
199+
200+
private static string ConvertPriorityEnumToString(CookiePriority priority)
201+
{
202+
return priority switch
203+
{
204+
CookiePriority.Low => "Low",
205+
CookiePriority.Medium => "Medium",
206+
CookiePriority.High => "High",
207+
_ => "Medium",
208+
};
209+
}
210+
}

0 commit comments

Comments
 (0)