This is the second day of my participation in Gwen Challenge

The project address implemented in this article.

It should be common practice for many companies to put some static resource files and log files of users on Seven Cows. Seven Cow also provides a graphical tool called Kodo Browser, which makes it easy for users to browse files stored on seven Cow servers.

When there is a need for customization, it can also be customized from kodo-Browser’s GitHub source code. We need to customize the tool (see Swift/object-c decryption for Meituan Logan) so that we can easily view the encrypted log files. But since I didn’t have any knowledge of Notes or anything like that, I had to go the other way, using Swift + SwiftUI to do something similar to Finder on the Mac.

If you want to access the files on Qiniu, the first thing you need to understand is how qiniu’s network protocol works. The good news is that Qiniu provides API documentation, the bad news is that I read it for a long time and couldn’t understand anything. So I had to look at the Java SDK to analyze the network protocol.

Qiniu provides SDKS of many languages, and there are few scenes of direct use of API. Therefore, I feel that Qiniu does not do a good job in the maintenance of API documents, of course, it does not rule out my limited ability.

Here are the three network protocols that Swift implements:

  1. Getting the Bucket list
  2. Get a list of files
  3. Obtaining File Information
import Foundation
import Moya

let RsQboxBaseURL = URL(string: "https://rs.qbox.me")!
let RsQiniuBaseURL = URL(string: "https://rs.qiniu.com")!
let RsfQiniuBaseURL = URL(string: "https://rsf.qbox.me")!

public enum QiniuRequest {
    case buckets
    case statInfo(bucket: String, fileKey: String?).case list(bucket: String, prefix: String)}extension QiniuRequest: TargetType {

    public var baseURL: URL {
        switch self {
        case .buckets:
            return RsQiniuBaseURL
        case .statInfo:
            return RsQboxBaseURL
        case .list:
            return RsfQiniuBaseURL}}public var path: String {
        switch self {
        case .list:
            return "/list"
        case .buckets:
            return "/buckets"
        case let .statInfo(bucket, fileKey):
            let key = UrlSafeBase64.encodedEntry(bucket: bucket, fileKey: fileKey) ?? ""
            return "/stat/\(key)"}}public var task: Task {
        switch self {
        case .buckets:
            return .requestPlain
        case .statInfo:
            return .requestPlain
        case .list:
            return .requestParameters(parameters: getPram() ?? [String : String](), encoding: URLEncoding.default)
        }
    }
    
    func getPram(a)- > [String : String]? {
        switch self {
        case let .list(bucket, prefix) :var pram = [String: String]()
            pram["bucket"] = bucket
            pram["delimiter"] = "/"
            if prefix.count > 0 {
                pram["prefix"] = prefix
            }
            
            return pram
        default:
            return nil}}public var headers: [String : String]? {
        var header = [String : String] ()var absoluteString = self.absoluteURL.absoluteString
        if let parameters = getPram(), parameters.count > 0 {
            absoluteString + = "?"
            
            var components: [(String.String)] = []

            for key in parameters.keys.sorted(by: <) {
                let value = parameters[key]!
                components + = queryComponents(fromKey: key, value: value)
            }
            absoluteString + = components.map { "\ [$0)=\ [The $1)" }.joined(separator: "&")}let url = URL(string: absoluteString) ?? absoluteURL
        debugPrint("url:\(url)")
        header["Authorization"] = QiniuTool.getToken(url: url)
        return header
    }
    

    public var method: Moya.Method {
        switch self {
        case .statInfo,
             .list,
             .buckets:
            return .get
        }
    }
}

extension QiniuRequest {
    /// Creates a percent-escaped, URL encoded query string components from the given key-value pair recursively.
    ///
    /// - Parameters:
    /// - key: Key of the query component.
    /// - value: Value of the query component.
    ///
    /// - Returns: The percent-escaped, URL encoded query string components.
    public func queryComponents(fromKey key: String.value: Any)- > [(String.String)] {
        var components: [(String.String)] = []
        switch value {
        case let dictionary as [String: Any] :for (nestedKey, value) in dictionary {
                components + = queryComponents(fromKey: "\(key)[\(nestedKey)]. "", value: value)
            }
        default:
            components.append((escape(key), escape("\(value)")))}return components
    }

    /// Creates a percent-escaped string following RFC 3986 for a query string key or value.
    ///
    /// - Parameter string: `String` to be percent-escaped.
    ///
    /// - Returns: The percent-escaped `String`.
    public func escape(_ string: String) -> String {
        string.addingPercentEncoding(withAllowedCharacters: .afURLQueryAllowed) ?? string
    }
}
Copy the code

There are three different hosts for the three network requests, which is supposed to relieve the pressure on the server.

Seven cow network protocol

The network protocol of Qiniu adds Authorization information to the header. The generation process of Authorization is as follows:

  1. Get the full request URL path;
  2. Sort all the request parameters, concatenate them after the URL path, concatenate a newline character “\n”, and encrypt them;
  3. The final concatenation is a string in the format “QBox QiniuAccessKey: encrypted result “.

Here is how to encrypt a string:

class QiniuTool { 
    static func getToken(url: URL) -> String? {
        var text = ""
        text + = url.path
        if let query = url.query {
            text + = "?\(query)"
        }
        text + = "\n"

        var digest = text.hmac(algorithm: .SHA1, key: QiniuConfig.QiniuSecretKey)
        digest = digest.replacingOccurrences(of: "/", with: "_")
        digest = digest.replacingOccurrences(of: "+", with: "-")
        return "QBox \(QiniuConfig.QiniuAccessKey):\(digest)"}}Copy the code

Seven cows temporary access link

To make it easy to download files, you can generate a temporary access link.

Generating a temporary access link for seven cows does not require a service request, but an encrypted token is added to the end of the access link, and the server verifies the token when accessing the server.

class QiniuTool {
    static func getPublicUrl(bucket: String = QiniuConfig.BucketDomain.key: String) -> String? {
        let e = Int(Date().timeIntervalSince1970 + 3600)
        let url = "https://\(bucket)/\(key)? e=\(e)"
        
        var r = url.hmac(algorithm: .SHA1, key: QiniuConfig.QiniuSecretKey)
        r = r.replacingOccurrences(of: "/", with: "_")
        r = r.replacingOccurrences(of: "+", with: "-")
    
        return "\(url)&token=\(QiniuConfig.QiniuAccessKey):\(r)"}}Copy the code

I used to think that this kind of link needs to be generated on the server to be safe, but it can be generated on the client.

Once again remind

See the full project address for this article.

When there is a need for customization, you can use the GitHub source code of Kodo-Browser to customize, or use a good SDK provided by Seven Cows to save a lot of trouble.