Drop unauthenticated guests from channels: the allow_guests setting, the /guests endpoints and the guest seat table are gone, and requests without an account session get 401 account_required. Share links are unaffected. Contract version is now 8.0.0.
106 lines
4.9 KiB
C#
106 lines
4.9 KiB
C#
using WebDrop.Core;
|
|
using WebDrop.Storage;
|
|
|
|
namespace WebDrop.Api;
|
|
|
|
/// <summary>从请求头(或 WebSocket 子协议)解析凭据。这里只做格式校验,令牌是否正确由服务判断。</summary>
|
|
public static class RequestAuth
|
|
{
|
|
public const string RoleHeader = "X-WD-Role";
|
|
public const string EndpointHeader = "X-WD-Endpoint";
|
|
public const string CaptchaHeader = "X-WD-Cap";
|
|
|
|
public static Credentials FromHeaders(HttpRequest req, bool requireEndpoint)
|
|
{
|
|
var role = ParseRole(req.Headers[RoleHeader].ToString());
|
|
|
|
byte[]? token = null;
|
|
string auth = req.Headers.Authorization.ToString();
|
|
if (auth.Length > 0)
|
|
{
|
|
const string prefix = "Bearer ";
|
|
if (!auth.StartsWith(prefix, StringComparison.Ordinal)
|
|
|| !Codec.TryDecodeExact(auth[prefix.Length..], TokenHash.Size, out token))
|
|
throw new WebDropException(ErrorCode.BadRequest);
|
|
}
|
|
|
|
string? accountName = null;
|
|
string? deviceId = null;
|
|
var accountToken = req.Headers["X-WD-Account"].ToString();
|
|
if (accountToken.Length > 0)
|
|
{
|
|
var accounts = req.HttpContext.RequestServices.GetRequiredService<SqliteAccountStore>();
|
|
if (!accounts.HasSession(accountToken, out var username)) throw new WebDropException(ErrorCode.AccountRequired);
|
|
accountName = username;
|
|
deviceId = accounts.SessionDevice(accountToken);
|
|
}
|
|
if (role == Role.Owner && accountName is null) throw new WebDropException(ErrorCode.AccountRequired);
|
|
// 账号以会话鉴权;终端密钥只用于便捷分享(未登录的创建者与读者)
|
|
return new Credentials(role, token, ParseEndpoint(req.Headers[EndpointHeader].ToString(), requireEndpoint && accountName is null), accountName, deviceId, accountName is not null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 浏览器的 WebSocket 不能设请求头,凭据放在子协议里:
|
|
/// wd.v1, wd.role.<role>, wd.auth.<b64url>, wd.ep.<b64url>
|
|
/// </summary>
|
|
public static Credentials FromSubprotocols(IEnumerable<string> protocols, SqliteAccountStore? accounts = null)
|
|
{
|
|
string? role = null, auth = null, endpoint = null, accountToken = null;
|
|
foreach (string p in protocols)
|
|
{
|
|
if (p.StartsWith("wd.role.", StringComparison.Ordinal)) role = p["wd.role.".Length..];
|
|
else if (p.StartsWith("wd.auth.", StringComparison.Ordinal)) auth = p["wd.auth.".Length..];
|
|
else if (p.StartsWith("wd.ep.", StringComparison.Ordinal)) endpoint = p["wd.ep.".Length..];
|
|
else if (p.StartsWith("wd.account.", StringComparison.Ordinal)) accountToken = p["wd.account.".Length..];
|
|
}
|
|
|
|
byte[]? token = null;
|
|
if (auth is not null && !Codec.TryDecodeExact(auth, TokenHash.Size, out token))
|
|
throw new WebDropException(ErrorCode.BadRequest);
|
|
|
|
string? accountName = null;
|
|
string? deviceId = null;
|
|
if (accountToken is not null)
|
|
{
|
|
if (accounts is null || !Codec.TryDecodeExact(accountToken, 32, out var raw)
|
|
|| !accounts.HasSession(Convert.ToBase64String(raw), out var username)) throw new WebDropException(ErrorCode.AccountRequired);
|
|
accountName = username;
|
|
deviceId = accounts.SessionDevice(Convert.ToBase64String(raw));
|
|
}
|
|
var parsedRole = ParseRole(role);
|
|
if (parsedRole == Role.Owner && accountName is null) throw new WebDropException(ErrorCode.AccountRequired);
|
|
return new Credentials(parsedRole, token, ParseEndpoint(endpoint, requireEndpoint: false), accountName, deviceId, accountName is not null);
|
|
}
|
|
|
|
public static byte[] ParseEndpointHash(string? header) => ParseEndpoint(header, requireEndpoint: true);
|
|
|
|
public static byte[] ParseEndpointHashOptional(string? header) => ParseEndpoint(header, requireEndpoint: false);
|
|
|
|
public static Credentials ShareFromHeaders(HttpRequest req, bool requireEndpoint)
|
|
{
|
|
var credentials = FromHeaders(req, requireEndpoint);
|
|
if (credentials.ClaimedRole == Role.Write) throw new WebDropException(ErrorCode.BadRequest);
|
|
return credentials;
|
|
}
|
|
|
|
private static Role ParseRole(string? value) => value switch
|
|
{
|
|
"read" => Role.Read,
|
|
"write" => Role.Write,
|
|
"owner" => Role.Owner,
|
|
_ => throw new WebDropException(ErrorCode.BadRequest),
|
|
};
|
|
|
|
/// <summary>终端密钥 S 为 32 字节,服务端只保存并使用 SHA-256(S)。缺省时为空,仅允许不需要席位的操作。</summary>
|
|
private static byte[] ParseEndpoint(string? value, bool requireEndpoint)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
if (requireEndpoint) throw new WebDropException(ErrorCode.BadRequest);
|
|
return [];
|
|
}
|
|
if (!Codec.TryDecodeExact(value, 32, out var secret)) throw new WebDropException(ErrorCode.BadRequest);
|
|
return TokenHash.Compute(secret);
|
|
}
|
|
}
|