diff --git a/src/Heartbeat.Runtime/Analyzers/LongStringAnalyzer.cs b/src/Heartbeat.Runtime/Analyzers/LongStringAnalyzer.cs index 9643e7c..7f5a3b4 100644 --- a/src/Heartbeat.Runtime/Analyzers/LongStringAnalyzer.cs +++ b/src/Heartbeat.Runtime/Analyzers/LongStringAnalyzer.cs @@ -62,7 +62,7 @@ public IReadOnlyCollection GetStrings(ObjectGCStatus? status, in foreach (var stringClrObject in GetLongestStrings(count, status)) { string value = stringClrObject.AsString(truncateLength ?? 4096)!; - var length = stringClrObject.ReadField("_stringLength"); + var length = stringClrObject.ReadField(Context.GetStringLengthFieldName()); var size = new Size(stringClrObject.Size); result.Add(new LongStringInfo(new(stringClrObject.Address), size, length, value)); } diff --git a/src/Heartbeat.Runtime/Analyzers/StringDuplicateAnalyzer.cs b/src/Heartbeat.Runtime/Analyzers/StringDuplicateAnalyzer.cs index 1082eec..d6985a9 100644 --- a/src/Heartbeat.Runtime/Analyzers/StringDuplicateAnalyzer.cs +++ b/src/Heartbeat.Runtime/Analyzers/StringDuplicateAnalyzer.cs @@ -64,7 +64,7 @@ from clrObject in Context.EnumerateStrings(ObjectGcStatus, Generation) } else { - var fullLength = stringInstance.ReadField("_stringLength"); + var fullLength = stringInstance.ReadField(Context.GetStringLengthFieldName()); stringCount[stringValue] = new StringDuplicateInfo(1, fullLength); } } diff --git a/src/Heartbeat.Runtime/RuntimeContext.cs b/src/Heartbeat.Runtime/RuntimeContext.cs index efc28b9..62c13f0 100644 --- a/src/Heartbeat.Runtime/RuntimeContext.cs +++ b/src/Heartbeat.Runtime/RuntimeContext.cs @@ -1,4 +1,3 @@ - using Heartbeat.Runtime.Domain; using Heartbeat.Runtime.Extensions; using Heartbeat.Runtime.Models; @@ -45,6 +44,13 @@ public string GetAutoPropertyFieldName(string propertyName) throw new NotImplementedException(); } + public string GetStringLengthFieldName() + { + return IsCoreRuntime + ? "_stringLength" + : "m_stringLength"; + } + public IEnumerable EnumerateObjectAddressesByTypeName(string typeName, ObjectGCStatus? status) { var clrType = Heap.GetTypeByName(typeName); @@ -74,7 +80,7 @@ where obj.IsValid } public IEnumerable EnumerateObjectsByTypeName( - string typeName, + string typeName, ObjectGCStatus? status, Generation? generation = null) { @@ -114,7 +120,7 @@ from clrObject in Heap.EnumerateObjects() where type != null && !type.IsFree && type.Name == "System.Threading.Thread" let managedThreadId = type.GetFieldByName("m_ManagedThreadId")!.Read(clrObject, true) let threadName = type.GetFieldByName("m_Name")!.ReadString(clrObject, false) - select new {managedThreadId, threadName}; + select new { managedThreadId, threadName }; // // <{0}>k__BackingField // var threadQuery = from address in heap.EnumerateObjectAddresses() @@ -195,7 +201,7 @@ public ulong GetWeakRefValue(ClrObject weakRefObject) var intPtrValueField = intPtrType.GetFieldByName(valueField); var handleAddr = weakRefHandleField.Read(weakRefObject.Address, true); - var value = intPtrValueField.Read((ulong) handleAddr, true); + var value = intPtrValueField.Read((ulong)handleAddr, true); return value; } diff --git a/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx b/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx index 579b665..12ad343 100644 --- a/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx +++ b/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx @@ -24,7 +24,9 @@ import Box from "@mui/material/Box"; // TODO add Dictionary, Queue, Stack and other collections view to a new tab // TODO add ConcurrentDictionary view to a new tab (dcd, dumpconcurrentdictionary
Displays concurrent dictionary content.) // TODO add ConcurrentQueue view to a new tab (dcq, dumpconcurrentqueue
Displays concurrent queue content.) -// TODO add JWT decode tab (https://github.com/panva/jose) (System.IdentityModel.Tokens.Jwt) +// TODO add base64 decode tab (base64 string -> utf8 string) | devenv dump - https://localhost:44443/#/clr-object/0000021d1b6452b8/show +// TODO add gzip string decode tab | devenv dump - https://localhost:44443/#/clr-object/0000021d1b6f6f80/show +// TODO add certificate decode tab // TODO find other debugger visualizers export const ClrObject = () => { @@ -92,6 +94,7 @@ export const ClrObject = () => { {title: 'Size', value: toSizeString(objectResult.size || 0)}, {title: 'Generation', value: objectResult.generation}, // TODO add Live / Dead + // TODO add ValueType / reference type {title: 'MethodTable', value: renderMethodTableLink(objectResult.methodTable)}, {title: 'Type', value: objectResult.typeName}, {title: 'Module', value: objectResult.moduleName}, diff --git a/src/Heartbeat/Endpoints/RouteHandlers.cs b/src/Heartbeat/Endpoints/RouteHandlers.cs index 629527c..4cd33a6 100644 --- a/src/Heartbeat/Endpoints/RouteHandlers.cs +++ b/src/Heartbeat/Endpoints/RouteHandlers.cs @@ -101,7 +101,7 @@ public static IEnumerable GetStrings( { var query = from obj in context.EnumerateStrings(gcStatus, generation) let str = obj.AsString() - let length = obj.ReadField("_stringLength") + let length = obj.ReadField(context.GetStringLengthFieldName()) select new StringInfo(obj.Address, length, obj.Size, str); // TODO limit output qty diff --git a/src/Heartbeat/Helpers/JwtParser.cs b/src/Heartbeat/Helpers/JwtParser.cs index 67aac26..ffa0fb2 100644 --- a/src/Heartbeat/Helpers/JwtParser.cs +++ b/src/Heartbeat/Helpers/JwtParser.cs @@ -96,10 +96,8 @@ internal static class JwtParser ["vp"] = "Verifiable Presentation as specified in the W3C Recommendation", ["sph"] = "SIP Priority header field", ["ace_profile"] = "The ACE profile a token is supposed to be used with.", - ["cnonce"] = - "\"client-nonce\". A nonce previously provided to the AS by the RS via the client. Used to verify token freshness when the RS cannot synchronize its clock with the AS.", - ["exi"] = - "\"Expires in\". Lifetime of the token in seconds from the time the RS first sees it. Used to implement a weaker from of token expiration for devices that cannot synchronize their internal clocks.", + ["cnonce"] = "\"client-nonce\". A nonce previously provided to the AS by the RS via the client. Used to verify token freshness when the RS cannot synchronize its clock with the AS.", + ["exi"] = "\"Expires in\". Lifetime of the token in seconds from the time the RS first sees it. Used to implement a weaker from of token expiration for devices that cannot synchronize their internal clocks.", ["roles"] = "Roles", ["groups"] = "Groups", ["entitlements"] = "Entitlements", @@ -133,23 +131,17 @@ internal static class JwtParser ["cdnistt"] = "CDNI Signed Token Transport Method for Signed Token Renewal", ["cdnistd"] = "CDNI Signed Token Depth", ["sig_val_claims"] = "Signature Validation Token", - ["authorization_details"] = - "The claim authorization_details contains a JSON array of JSON objects representing the rights of the access token. Each JSON object contains the data to specify the authorization requirements for a certain type of resource.", - ["verified_claims"] = - "This container Claim is composed of the verification evidence related to a certain verification process and the corresponding Claims about the End-User which were verified in this process.", + ["authorization_details"] = "The claim authorization_details contains a JSON array of JSON objects representing the rights of the access token. Each JSON object contains the data to specify the authorization requirements for a certain type of resource.", + ["verified_claims"] = "This container Claim is composed of the verification evidence related to a certain verification process and the corresponding Claims about the End-User which were verified in this process.", ["place_of_birth"] = "A structured Claim representing the End-User's place of birth.", ["nationalities"] = "String array representing the End-User's nationalities.", - ["birth_family_name"] = - "Family name(s) someone has when they were born, or at least from the time they were a child. This term can be used by a person who changes the family name(s) later in life for any reason. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters.", - ["birth_given_name"] = - "Given name(s) someone has when they were born, or at least from the time they were a child. This term can be used by a person who changes the given name later in life for any reason. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters.", - ["birth_middle_name"] = - "Middle name(s) someone has when they were born, or at least from the time they were a child. This term can be used by a person who changes the middle name later in life for any reason. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used.", + ["birth_family_name"] = "Family name(s) someone has when they were born, or at least from the time they were a child. This term can be used by a person who changes the family name(s) later in life for any reason. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters.", + ["birth_given_name"] = "Given name(s) someone has when they were born, or at least from the time they were a child. This term can be used by a person who changes the given name later in life for any reason. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters.", + ["birth_middle_name"] = "Middle name(s) someone has when they were born, or at least from the time they were a child. This term can be used by a person who changes the middle name later in life for any reason. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used.", ["salutation"] = "End-User's salutation, e.g., \"Mr.\"", ["title"] = "End-User's title, e.g., \"Dr.\"", ["msisdn"] = "End-User's mobile phone number formatted according to ITU-T recommendation [E.164]", - ["also_known_as"] = - "Stage name, religious name or any other type of alias/pseudonym with which a person is known in a specific context besides its legal name. This must be part of the applicable legislation and thus the trust framework (e.g., be an attribute on the identity card).", + ["also_known_as"] = "Stage name, religious name or any other type of alias/pseudonym with which a person is known in a specific context besides its legal name. This must be part of the applicable legislation and thus the trust framework (e.g., be an attribute on the identity card).", ["htm"] = "The HTTP method of the request", ["htu"] = "The HTTP URI of the request (without query and fragment parts)", ["ath"] = "The base64url-encoded SHA-256 hash of the ASCII encoding of the associated access token's value", @@ -161,10 +153,8 @@ internal static class JwtParser ["msgi"] = "Message Integrity Information", ["_claim_names"] = "JSON object whose member names are the Claim Names for the Aggregated and Distributed Claims", ["_claim_sources"] = "JSON object whose member names are referenced by the member values of the _claim_names member", - ["rdap_allowed_purposes"] = - "This claim describes the set of RDAP query purposes that are available to an identity that is presented for access to a protected RDAP resource.", - ["rdap_dnt_allowed"] = - "This claim contains a JSON boolean literal that describes a \"do not track\" request for server-side tracking, logging, or recording of an identity that is presented for access to a protected RDAP resource.", + ["rdap_allowed_purposes"] = "This claim describes the set of RDAP query purposes that are available to an identity that is presented for access to a protected RDAP resource.", + ["rdap_dnt_allowed"] = "This claim contains a JSON boolean literal that describes a \"do not track\" request for server-side tracking, logging, or recording of an identity that is presented for access to a protected RDAP resource.", }.ToFrozenDictionary(); public static JwtInfo ToJwtInfo(string str) @@ -183,18 +173,28 @@ public static JwtInfo ToJwtInfo(string str) var headerValues = header .Select(kvp => new JwtValue(kvp.Key, kvp.Value, _headerKeyDescription.GetValueOrDefault(kvp.Key))) .ToArray(); - + var payloadValues = payload .Select(kvp => new JwtValue( kvp.Key, - _numericDateClaimKeys.Contains(kvp.Key) - ? $"{kvp.Value} ({DateTimeOffset.FromUnixTimeSeconds((long)(decimal)kvp.Value):R})" - : kvp.Value.ToString()!, + GetPayloadValue(kvp), _payloadKeyDescription.GetValueOrDefault(kvp.Key))) .ToArray(); var result = new JwtInfo(headerValues, payloadValues); return result; } + + private static string GetPayloadValue(KeyValuePair kvp) + { + if (kvp.Value is List list) + { + return $"[{string.Join(',', list)}]"; + } + + return _numericDateClaimKeys.Contains(kvp.Key) + ? $"{kvp.Value} ({DateTimeOffset.FromUnixTimeSeconds((long)(decimal)kvp.Value):R})" + : kvp.Value.ToString()!; + } } [JsonSerializable(typeof(JWT.Builder.JwtHeader))]