Skip to content

Commit 4ec4c6c

Browse files
committed
masked headers
1 parent 8101999 commit 4ec4c6c

File tree

5 files changed

+58
-24
lines changed

5 files changed

+58
-24
lines changed

Sources/SwiftAPIClient/APIClientCaller.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ public extension APIClient {
270270
let message = configs._errorLoggingComponents.errorMessage(
271271
uuid: uuid,
272272
error: error,
273+
maskedHeaders: configs.logMaskedHeaders,
273274
fileIDLine: fileIDLine
274275
)
275276
configs.logger.log(level: configs._errorLogLevel, "\(message)")

Sources/SwiftAPIClient/Clients/HTTPClient.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ extension APIClientCaller where Result == AsyncThrowingValue<(Value, HTTPRespons
137137
error: error,
138138
request: request,
139139
duration: duration,
140+
maskedHeaders: configs.logMaskedHeaders,
140141
fileIDLine: configs.fileIDLine
141142
)
142143
configs.logger.log(level: configs._errorLogLevel, "\(message)")
@@ -174,6 +175,7 @@ extension APIClientCaller where Result == AsyncThrowingValue<(Value, HTTPRespons
174175
request: request,
175176
data: data,
176177
duration: duration,
178+
maskedHeaders: configs.logMaskedHeaders,
177179
fileIDLine: configs.fileIDLine
178180
)
179181
configs.logger.log(
@@ -196,6 +198,7 @@ extension APIClientCaller where Result == AsyncThrowingValue<(Value, HTTPRespons
196198
data: data,
197199
duration: duration,
198200
error: error,
201+
maskedHeaders: configs.logMaskedHeaders,
199202
fileIDLine: configs.fileIDLine
200203
)
201204
configs.logger.log(level: configs._errorLogLevel, "\(message)")

Sources/SwiftAPIClient/Modifiers/LoggingModifier.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Logging
3+
import HTTPTypesFoundation
34

45
public extension APIClient {
56

@@ -35,6 +36,14 @@ public extension APIClient {
3536
func errorLoggingComponents(_ components: LoggingComponents?) -> APIClient {
3637
configs(\.errorLogginComponents, components)
3738
}
39+
40+
/// Sets the headers that should be masked in logs.
41+
/// - Parameter headers: A `Set<HTTPField.Name>` containing the names of headers to be masked.
42+
func logMaskedHeaders(_ headers: Set<HTTPField.Name>) -> APIClient {
43+
configs { configs in
44+
configs.logMaskedHeaders.formUnion(headers)
45+
}
46+
}
3847
}
3948

4049
public extension APIClient.Configs {
@@ -73,6 +82,13 @@ public extension APIClient.Configs {
7382
get { self[\.errorLogginComponents] ?? nil }
7483
set { self[\.errorLogginComponents] = newValue }
7584
}
85+
86+
/// The headers that should be masked in logs.
87+
/// - Returns: A `Set<HTTPField.Name>` containing the names of headers to be masked.
88+
var logMaskedHeaders: Set<HTTPField.Name> {
89+
get { self[\.logMaskedHeaders] ?? [.authorization] }
90+
set { self[\.logMaskedHeaders] = newValue }
91+
}
7692
}
7793

7894
extension APIClient.Configs {
@@ -87,7 +103,7 @@ extension APIClient.Configs {
87103

88104
func logRequest(_ request: HTTPRequestComponents, uuid: UUID) {
89105
if loggingComponents.contains(.onRequest), loggingComponents != .onRequest {
90-
let message = loggingComponents.requestMessage(for: request, uuid: uuid, fileIDLine: fileIDLine)
106+
let message = loggingComponents.requestMessage(for: request, uuid: uuid, maskedHeaders: logMaskedHeaders, fileIDLine: fileIDLine)
91107
logger.log(level: logLevel, "\(message)")
92108
}
93109
#if canImport(Metrics)

Sources/SwiftAPIClient/Types/HTTPRequestComponents.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,13 @@ public extension HTTPRequestComponents {
346346

347347
public extension HTTPRequestComponents {
348348

349+
/// Returns a cURL command string representation of the request
350+
var cURL: String {
351+
cURL(maskedHeaders: [])
352+
}
353+
349354
/// Returns a cURL command string representation of the request
350-
var cURL: String {
355+
func cURL(maskedHeaders: Set<HTTPField.Name>) -> String {
351356
var components: [String] = []
352357

353358
// Add URL
@@ -364,7 +369,7 @@ public extension HTTPRequestComponents {
364369
// Add headers
365370
for field in headers {
366371
let headerValue = field.value.replacingOccurrences(of: "\"", with: "\\\"") // Escape double quotes
367-
components.append("-H \"\(field.name.rawName): \(headerValue)\"")
372+
components.append("-H \"\(field.name.rawName): \(maskedHeaders.contains(field.name) ? "***" : headerValue)\"")
368373
}
369374

370375
// Add body if present (support multiple -d flags)
@@ -436,4 +441,4 @@ private let urlPattern = #"(?:\s+|^)['\"]?(https?://[^\s'\"\\]+)['\"]?"#
436441
private let headerPattern = #"(?:-H\s+|--header\s+)(?:'([^']+:\s*[^']*)'|"([^"]+:\s*[^"]*)"|([^\s'"]+:\s*[^\s'"]+))"#
437442

438443
/// Regular expression pattern to match data in cURL command (support multiple `-d`)
439-
private let dataPattern = #"(?:-d\s+|--data(?:-ascii|-binary|-raw|-urlencode)?\s?)(?:'([^']*)'|"([^"]*)"|([^\s'"]+))"#
444+
private let dataPattern = #"(?:-d\s+|--data(?:-ascii|-binary|-raw|-urlencode)?\s?)(?:'([^']*)'|"([^"]*)"|([^\s'"]+))"#

Sources/SwiftAPIClient/Types/LoggingComponent.swift

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22
#if canImport(FoundationNetworking)
33
import FoundationNetworking
44
#endif
5+
import HTTPTypes
56

67
/// The components to be logged.
78
public struct LoggingComponents: OptionSet {
@@ -17,8 +18,8 @@ public struct LoggingComponents: OptionSet {
1718
public static let query = LoggingComponents(rawValue: 1 << 8)
1819
public static let uuid = LoggingComponents(rawValue: 1 << 9)
1920
public static let location = LoggingComponents(rawValue: 1 << 10)
20-
public static let onRequest = LoggingComponents(rawValue: 1 << 11)
21-
public static let cURL = LoggingComponents(rawValue: 1 << 12)
21+
public static let onRequest = LoggingComponents(rawValue: 1 << 11)
22+
public static let cURL = LoggingComponents(rawValue: 1 << 12)
2223

2324
public static var url: LoggingComponents { [.path, .baseURL, .query] }
2425

@@ -71,6 +72,7 @@ public extension LoggingComponents {
7172
func requestMessage(
7273
for request: HTTPRequestComponents,
7374
uuid: UUID,
75+
maskedHeaders: Set<HTTPField.Name>,
7476
fileIDLine: FileIDLine?
7577
) -> String {
7678
guard !isEmpty else { return "" }
@@ -92,7 +94,7 @@ public extension LoggingComponents {
9294
message += " (\(body.count)-byte body)"
9395
}
9496
if contains(.headers), !request.headers.isEmpty {
95-
message += "\n\(request.headers.multilineDescription)"
97+
message += "\n\(request.headers.multilineDescription(masked: maskedHeaders))"
9698
isMultiline = true
9799
}
98100
if contains(.body), let body = request.body?.data, let bodyString = String(data: body, encoding: .utf8) {
@@ -104,7 +106,7 @@ public extension LoggingComponents {
104106
isMultiline = true
105107
}
106108
if contains(.cURL) {
107-
message += "\n\(request.cURL)"
109+
message += "\n\(request.cURL(maskedHeaders: maskedHeaders))"
108110
isMultiline = true
109111
}
110112
if isMultiline {
@@ -123,7 +125,8 @@ public extension LoggingComponents {
123125
data: Data?,
124126
duration: TimeInterval,
125127
error: Error? = nil,
126-
fileIDLine: FileIDLine?
128+
maskedHeaders: Set<HTTPField.Name>,
129+
fileIDLine: FileIDLine?
127130
) -> String {
128131
responseMessage(
129132
uuid: uuid,
@@ -133,7 +136,8 @@ public extension LoggingComponents {
133136
headers: response.headerFields,
134137
duration: duration,
135138
error: error,
136-
fileIDLine: fileIDLine
139+
maskedHeaders: maskedHeaders,
140+
fileIDLine: fileIDLine
137141
)
138142
}
139143

@@ -145,17 +149,18 @@ public extension LoggingComponents {
145149
headers: HTTPFields = [:],
146150
duration: TimeInterval? = nil,
147151
error: Error? = nil,
148-
fileIDLine: FileIDLine?
152+
maskedHeaders: Set<HTTPField.Name>,
153+
fileIDLine: FileIDLine?
149154
) -> String {
150155
guard !isEmpty else { return "" }
151-
var message = "<-- "
152-
if let request {
153-
message = requestMessage(for: request, uuid: uuid, fileIDLine: fileIDLine) + "\n" + message
154-
} else {
155-
if contains(.uuid) {
156-
message = "[\(uuid.uuidString)]\n" + message
157-
}
158-
}
156+
var message = "<-- "
157+
if let request {
158+
message = requestMessage(for: request, uuid: uuid, maskedHeaders: maskedHeaders, fileIDLine: fileIDLine) + "\n" + message
159+
} else {
160+
if contains(.uuid) {
161+
message = "[\(uuid.uuidString)]\n" + message
162+
}
163+
}
159164
switch (statusCode?.kind, error) {
160165
case (_, .some), (.serverError, _), (.clientError, _), (.invalid, _):
161166
message.append("🛑")
@@ -187,7 +192,7 @@ public extension LoggingComponents {
187192
}
188193

189194
if contains(.headers), !headers.isEmpty {
190-
message += "\n\(headers.multilineDescription)"
195+
message += "\n\(headers.multilineDescription(masked: maskedHeaders))"
191196
isMultiline = true
192197
}
193198
if contains(.body), let body = data, let bodyString = String(data: body, encoding: .utf8) {
@@ -207,6 +212,7 @@ public extension LoggingComponents {
207212
error: Error,
208213
request: HTTPRequestComponents? = nil,
209214
duration: TimeInterval? = nil,
215+
maskedHeaders: Set<HTTPField.Name>,
210216
fileIDLine: FileIDLine? = nil
211217
) -> String {
212218
var message = contains(.uuid) && request == nil ? "[\(uuid.uuidString)] " : ""
@@ -215,8 +221,8 @@ public extension LoggingComponents {
215221
}
216222

217223
if let request {
218-
message = requestMessage(for: request, uuid: uuid, fileIDLine: fileIDLine) + "\n" + message
219-
} else if let fileIDLine, contains(.location) {
224+
message = requestMessage(for: request, uuid: uuid, maskedHeaders: maskedHeaders, fileIDLine: fileIDLine) + "\n" + message
225+
} else if let fileIDLine, contains(.location) {
220226
message = "\(fileIDLine.fileID)/\(fileIDLine.line)\n" + message
221227
}
222228
message += "❗️\(error.humanReadable)❗️"
@@ -248,7 +254,10 @@ public extension LoggingComponents {
248254

249255
private extension HTTPFields {
250256

251-
var multilineDescription: String {
252-
map { "\($0.name): \($0.value)" }.joined(separator: "\n")
257+
func multilineDescription(masked: Set<HTTPField.Name>) -> String {
258+
map {
259+
"\($0.name): \(masked.contains($0.name) ? "***" : $0.value)"
260+
}
261+
.joined(separator: "\n")
253262
}
254263
}

0 commit comments

Comments
 (0)