Skip to content

Commit

Permalink
Merge pull request #8 from tkausch/InsertLogging
Browse files Browse the repository at this point in the history
Use "swift-log" to introduce platform independent  logging support
  • Loading branch information
tkausch authored May 22, 2024
2 parents e8da9d8 + d7de980 commit f99fc2d
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 32 deletions.
9 changes: 5 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ let package = Package(
.macOS(.v12)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "SwiftRestRequests",
targets: ["SwiftRestRequests"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log", from: "1.5.4"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "SwiftRestRequests"),
name: "SwiftRestRequests",
dependencies: [.product(name: "Logging", package: "swift-log")]),
.testTarget(
name: "SwiftRestRequestsTests",
dependencies: ["SwiftRestRequests"]),
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftRestRequests/RestApiCaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ open class RestApiCaller : NSObject {
/// - options: Rest options to use for the data task i.e. timeout
/// - Returns: The data returned by server and the corresponding `HTTPURLResponse`
private func makeCall<T: Deserializer>(_ relativePath: String?, httpMethod: HTTPMethod, payload: Data?, responseDeserializer: T, options: RestOptions) async throws -> (T.ResponseType?, HTTPStatusCode) {

let (data, httpResponse) = try await dataTask(relativePath: relativePath, httpMethod: httpMethod.rawValue, accept: responseDeserializer.acceptHeader, payload: payload, options: options)

let httpStatus = httpResponse.status
Expand Down
29 changes: 17 additions & 12 deletions Sources/SwiftRestRequests/interceptors/LogNetworkInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@

import Foundation

#if os(Linux)
// no network tracing is implemented
#else
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif


import OSLog
import Logging

open class LogNetworkInterceptor: URLRequestInterceptor {

Expand All @@ -36,17 +37,19 @@ open class LogNetworkInterceptor: URLRequestInterceptor {
guard let requestHeaders = request.allHTTPHeaderFields,
let headerData = try? JSONSerialization.data(withJSONObject: requestHeaders , options: .prettyPrinted),
let prettyJsonHeaders = String(data: headerData , encoding: .utf8) else {
OSLog.interceptorLogger.warning("Something went wrong while converting headers to JSON data.")
Logger.interceptorLogger.warning("Something went wrong while converting headers to JSON data.")
return
}

let prettyJsonBody = request.httpBody?.prettyPrintedJSONString
let prettyJsonBody = request.httpBody?.prettyPrintedJSONString ?? "nil"

let url = request.url?.absoluteString ?? "nil"
let method = request.httpMethod ?? "nil"

OSLog.interceptorLogger.info("invokeRequest: \(method) \(url)")
OSLog.interceptorLogger.debug("invokeRequest: \(method) \(url) \nHTTP-HEADERS: \(prettyJsonHeaders) \nHTTP-BODY: \(prettyJsonBody ?? LogNetworkInterceptor.noBody)")
Logger.interceptorLogger.info("Will invoke request: \(method) \(url)")

Logger.interceptorLogger.trace("HTTP request headers: \(prettyJsonHeaders)")
Logger.interceptorLogger.trace("HTTP request body: \(prettyJsonBody)")
}

public func receiveResponse(data: Data, response: HTTPURLResponse, for session: URLSession) {
Expand All @@ -58,14 +61,16 @@ open class LogNetworkInterceptor: URLRequestInterceptor {
}


let prettyJsonBody = data.prettyPrintedJSONString
let prettyJsonBody = data.prettyPrintedJSONString ?? "nil"

let url = response.url?.absoluteString ?? "nil"
let status = response.statusCode


OSLog.interceptorLogger.info("receiveResponse: \(url) -> \(status)")
OSLog.interceptorLogger.debug("receiveResponse: \(url) -> \(status) \nHTTP-HEADERS: \(prettyJsonHeaders) \nHTTP-BODY: \(prettyJsonBody ?? LogNetworkInterceptor.noBody)")
Logger.interceptorLogger.info("Did receive response: \(url) -> \(status)")

Logger.interceptorLogger.trace("HTTP response headers: \(prettyJsonHeaders)")
Logger.interceptorLogger.trace("HTTP response body: \(prettyJsonBody)")
}
}

Expand All @@ -85,5 +90,5 @@ extension Data {
}
}

#endif


Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// OSLog+Extension.swift
// Logger+Extension.swift
//
// This File belongs to SwiftRestRequests
// Copyright © 2024 Thomas Kausch.
Expand All @@ -20,22 +20,16 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import Foundation

#if os(Linux)
// no network tracing is implemented
#else
import Logging

import OSLog
extension Logger {

extension OSLog {

static let subsystem = Bundle.main.bundleIdentifier!
static let labelPrefix = "com.swisscom.swiftRestRequests."

static let interceptorLogger = Logger(subsystem: subsystem, category: "SwiftRestRequestsInterceptor")
static let securityLogger = Logger(subsystem: subsystem, category: "SwiftRestRequestsSecurity")
static let apiCallerLogger = Logger(subsystem: subsystem, category: "SwiftRestRequestsApiCaller")
static let httpSessionLogger = Logger(subsystem: subsystem, category: "SwiftRestRequestsHttp")
public static var interceptorLogger = Logger(label: labelPrefix + "Interceptor")
public static var securityLogger = Logger(label: labelPrefix + "Security")
public static var apiCallerLogger = Logger(label: labelPrefix + "ApiCaller")

}


#endif
117 changes: 117 additions & 0 deletions Sources/SwiftRestRequests/logging/OSLogHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// OSLogHandler.swift
//
// This File belongs to SwiftRestRequests
// Copyright © 2024 Thomas Kausch.
// All Rights Reserved.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import Foundation
import Logging
import struct Logging.Logger

#if os(Linux)
// this package is not available for Linux - as it is using Apple os module
#else

import os

public struct OSLogHandler: LogHandler {
public var logLevel: Logger.Level = .info
public let label: String
private let oslogger: OSLog

public init(label: String) {
self.label = label
self.oslogger = OSLog(subsystem: label, category: "")
}

public init(label: String, log: OSLog) {
self.label = label
self.oslogger = log
}

public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt) {
var combinedPrettyMetadata = self.prettyMetadata
if let metadataOverride = metadata, !metadataOverride.isEmpty {
combinedPrettyMetadata = self.prettify(
self.metadata.merging(metadataOverride) {
return $1
}
)
}

var formedMessage = message.description
if combinedPrettyMetadata != nil {
formedMessage += " -- " + combinedPrettyMetadata!
}
os_log("%{public}@", log: self.oslogger, type: OSLogType.from(loggerLevel: level), formedMessage as NSString)
}

private var prettyMetadata: String?
public var metadata = Logger.Metadata() {
didSet {
self.prettyMetadata = self.prettify(self.metadata)
}
}

/// Add, remove, or change the logging metadata.
/// - parameters:
/// - metadataKey: the key for the metadata item.
public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
get {
return self.metadata[metadataKey]
}
set {
self.metadata[metadataKey] = newValue
}
}

private func prettify(_ metadata: Logger.Metadata) -> String? {
if metadata.isEmpty {
return nil
}
return metadata.map {
"\($0)=\($1)"
}.joined(separator: " ")
}
}

extension OSLogType {
static func from(loggerLevel: Logger.Level) -> Self {
switch loggerLevel {
case .trace:
/// `OSLog` doesn't have `trace`, so use `debug`
return .debug
case .debug:
return .debug
case .info:
return .info
case .notice:
// https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code
// According to the documentation, `default` is `notice`.
return .default
case .warning:
/// `OSLog` doesn't have `warning`, so use `info`
return .info
case .error:
return .error
case .critical:
return .fault
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@ import Foundation
// no public key pinning implemented
#else

import Logging

open class CertificateCAPinning: NSObject, URLSessionDelegate {

let pinnedCACertificates: [SecCertificate]

public init(pinnedCACertificates: [SecCertificate]) {
self.pinnedCACertificates = pinnedCACertificates
Logger.securityLogger.info("Initialized CertificateCAPinning with \(pinnedCACertificates)")
super.init()
}

public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {

guard let serverTrust = challenge.protectionSpace.serverTrust else {
Logger.securityLogger.error("Could not get serverTrust! Will cancel authentication.")
return (.cancelAuthenticationChallenge, nil)
}

Expand All @@ -50,8 +54,10 @@ open class CertificateCAPinning: NSObject, URLSessionDelegate {
let status = SecTrustEvaluateWithError(serverTrust, &error)

if error == nil && status {
return (.useCredential, URLCredential(trust: serverTrust))
Logger.securityLogger.info("ServerTrust evaluation was successful. Will proceed.")
return (.useCredential, URLCredential(trust: serverTrust))
} else {
Logger.securityLogger.error("ServerTrustevaluation evaluation failed. Will cancel the request.")
return (.cancelAuthenticationChallenge, nil)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import Foundation
// no public key pinning implemented
#else

import Logging

/// Use this URLSession delegate to implement public key server pinning.
/// Note: You need to assign this object as delegate for the `URLSession` object.
open class PublicKeyServerPinning: NSObject, URLSessionDelegate {
Expand All @@ -39,6 +41,7 @@ open class PublicKeyServerPinning: NSObject, URLSessionDelegate {
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {

guard let serverTrust = challenge.protectionSpace.serverTrust else {
Logger.securityLogger.error("Could not get serverTrust! Will cancel authentication.")
return(.cancelAuthenticationChallenge, nil)
}

Expand All @@ -48,11 +51,14 @@ open class PublicKeyServerPinning: NSObject, URLSessionDelegate {
if pinnedPublicKeys.contains(where: { publicKey in
publicKey == serverPublicKey
}) {
Logger.securityLogger.info("Trust evaluation was successful. The public key is known (pinned).")
return (.useCredential, URLCredential(trust: serverTrust))
} else {
Logger.securityLogger.error("Trust evaluation failed. The public key is unkown therfore cancel request.")
return (.cancelAuthenticationChallenge, nil)
}
} else {
Logger.securityLogger.error("Trust evaluation failed. No public key found on server. Cancel request.")
return (.cancelAuthenticationChallenge, nil)
}
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/SwiftRestRequests/security/URLRequestAuthorizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@


import Foundation
import Logging

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif



/// `URLRequestAuthenticator` will a authentication
public protocol URLRequestAuthorizer {
/// Configures URL request authorization header
Expand Down Expand Up @@ -55,6 +58,7 @@ public class BasicRequestAuthorizer: URLRequestAuthorizer {
}

public func configureAuthorizationHeader(for urlRequest: inout URLRequest) {
Logger.securityLogger.trace("Set HTTP Authorization header is set to: \(self.headerValue)")
urlRequest.setValue(self.headerValue, forHTTPHeaderField: "Authorization")
}
}
Expand All @@ -72,6 +76,7 @@ public class BearerReqeustAuthorizer: URLRequestAuthorizer {
}

public func configureAuthorizationHeader(for urlRequest: inout URLRequest) {
Logger.securityLogger.trace("Set HTTP Authorization header with bearer: \(self.token)")
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
Expand Down
38 changes: 38 additions & 0 deletions Tests/SwiftRestRequestsTests/AbstractRestApiCallerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// This File belongs to SwiftRestEssentials
// Copyright © 2024 Thomas Kausch.
// All Rights Reserved.

import XCTest
@testable import SwiftRestRequests


import Logging


class AbstractRestApiCallerTests: XCTestCase {

// When available we prefere to use the OSLog
static var onceExecution: () = {

#if os(Linux)
// Configure `swift-log`default logger
#else
/// Configure `swift-log` logging system to use OSLog backend
LoggingSystem.bootstrap(OSLogHandler.init)
#endif

Logger.securityLogger.logLevel = .trace
Logger.interceptorLogger.logLevel = .trace
Logger.apiCallerLogger.logLevel = .trace

}()


override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
let _ = AbstractRestApiCallerTests.onceExecution

}

}
Loading

0 comments on commit f99fc2d

Please sign in to comment.