浏览代码

Datastore example obtains credentials with OAuth and makes an initial Datastore API call

Tim Burks 9 年之前
父节点
当前提交
084463541d

+ 12 - 0
Examples/Datastore/Datastore.xcodeproj/project.pbxproj

@@ -11,6 +11,9 @@
 		D35C9FAE1D74B079000443CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D35C9FAD1D74B079000443CD /* Assets.xcassets */; };
 		D35C9FB11D74B079000443CD /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D35C9FAF1D74B079000443CD /* MainMenu.xib */; };
 		D35C9FC81D74B0C1000443CD /* DatastoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35C9FC71D74B0C1000443CD /* DatastoreViewController.swift */; };
+		D37BB8411D90A60300BFAF0D /* OAuthClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37BB8401D90A60300BFAF0D /* OAuthClient.swift */; };
+		D37BB8461D90ADAC00BFAF0D /* WebViewWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = D37BB8451D90ADAC00BFAF0D /* WebViewWindow.xib */; };
+		D37BB8481D90ADE300BFAF0D /* WebViewWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D37BB8471D90ADE300BFAF0D /* WebViewWindowController.swift */; };
 		D3B8C1CA1D906C16003FF8F8 /* gRPC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3B8C1B61D906BDF003FF8F8 /* gRPC.framework */; };
 		D3B8C1CB1D906C16003FF8F8 /* QuickProto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3B8C1BF1D906BEB003FF8F8 /* QuickProto.framework */; };
 		D3B8C1CD1D9074B0003FF8F8 /* roots.pem in Resources */ = {isa = PBXBuildFile; fileRef = D3B8C1CC1D9074B0003FF8F8 /* roots.pem */; };
@@ -76,6 +79,9 @@
 		D35C9FB01D74B079000443CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
 		D35C9FB21D74B079000443CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		D35C9FC71D74B0C1000443CD /* DatastoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatastoreViewController.swift; sourceTree = "<group>"; };
+		D37BB8401D90A60300BFAF0D /* OAuthClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuthClient.swift; sourceTree = "<group>"; };
+		D37BB8451D90ADAC00BFAF0D /* WebViewWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = WebViewWindow.xib; sourceTree = "<group>"; };
+		D37BB8471D90ADE300BFAF0D /* WebViewWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewWindowController.swift; sourceTree = "<group>"; };
 		D3B8C1B11D906BDF003FF8F8 /* gRPC.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = gRPC.xcodeproj; path = ../../gRPC.xcodeproj; sourceTree = "<group>"; };
 		D3B8C1B71D906BEA003FF8F8 /* QuickProto.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = QuickProto.xcodeproj; path = ../../QuickProto/QuickProto.xcodeproj; sourceTree = "<group>"; };
 		D3B8C1CC1D9074B0003FF8F8 /* roots.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = roots.pem; sourceTree = "<group>"; };
@@ -123,6 +129,9 @@
 				D35C9FAF1D74B079000443CD /* MainMenu.xib */,
 				D35C9FB21D74B079000443CD /* Info.plist */,
 				D35C9FC71D74B0C1000443CD /* DatastoreViewController.swift */,
+				D37BB8401D90A60300BFAF0D /* OAuthClient.swift */,
+				D37BB8451D90ADAC00BFAF0D /* WebViewWindow.xib */,
+				D37BB8471D90ADE300BFAF0D /* WebViewWindowController.swift */,
 			);
 			path = Datastore;
 			sourceTree = "<group>";
@@ -260,6 +269,7 @@
 				D3D2EA381D75FB4A002EF89C /* descriptors.out in Resources */,
 				D35C9FAE1D74B079000443CD /* Assets.xcassets in Resources */,
 				D35C9FB11D74B079000443CD /* MainMenu.xib in Resources */,
+				D37BB8461D90ADAC00BFAF0D /* WebViewWindow.xib in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -270,6 +280,8 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				D37BB8411D90A60300BFAF0D /* OAuthClient.swift in Sources */,
+				D37BB8481D90ADE300BFAF0D /* WebViewWindowController.swift in Sources */,
 				D35C9FC81D74B0C1000443CD /* DatastoreViewController.swift in Sources */,
 				D35C9FAC1D74B079000443CD /* AppDelegate.swift in Sources */,
 			);

+ 19 - 12
Examples/Datastore/Datastore/DatastoreViewController.swift

@@ -34,8 +34,7 @@ import AppKit
 import gRPC
 import QuickProto
 
-let APIKey = "AIzaSyDBqO88sO7iXScpFzmP7nOdamVYi7Y2Lbs"
-let Host = "datastore.googleapis.com"
+let Host = "datastore.googleapis.com:443"
 
 class DatastoreViewController : NSViewController, NSTextFieldDelegate {
   @IBOutlet weak var messageField: NSTextField!
@@ -46,12 +45,15 @@ class DatastoreViewController : NSViewController, NSTextFieldDelegate {
   private var client: Client?
   private var call: Call?
 
+  private var webViewWindowController : WebViewWindowController!
+
   required init?(coder:NSCoder) {
     fileDescriptorSet = FileDescriptorSet(filename: "descriptors.out")
     super.init(coder:coder)
   }
 
   var enabled = false
+  var authClient : OAuthClient!
 
   @IBAction func messageReturnPressed(sender: NSTextField) {
     if enabled {
@@ -59,14 +61,18 @@ class DatastoreViewController : NSViewController, NSTextFieldDelegate {
     }
   }
 
-  @IBAction func addressReturnPressed(sender: NSTextField) {
-  }
-
-  @IBAction func buttonValueChanged(sender: NSButton) {
-  }
-
   override func viewDidLoad() {
     gRPC.initialize()
+
+    authClient = OAuthClient()
+    let url = authClient.authCodeURL(state:"123")!
+
+    webViewWindowController = WebViewWindowController(windowNibName:"WebViewWindow")
+    webViewWindowController.url = url
+    webViewWindowController.completion = { (code) in
+      self.authClient.exchangeCode(code:code)
+    }
+    webViewWindowController.loadWindow()
   }
 
   override func viewDidAppear() {
@@ -82,10 +88,11 @@ class DatastoreViewController : NSViewController, NSTextFieldDelegate {
 
   func callServer(address:String) {
     let requestHost = Host
-    let requestMetadata = Metadata(["x-goog-api-key":APIKey,
-                                    "x-ios-bundle-identifier":Bundle.main.bundleIdentifier!])
 
-    // NONSTREAMING
+    let authorization = "Bearer " + authClient.token!
+
+    let requestMetadata = Metadata(["authorization":authorization])
+
     if let requestMessage = self.fileDescriptorSet.createMessage("RunQueryRequest") {
       requestMessage.addField("project_id", value:"hello-86")
       let gqlQuery = self.fileDescriptorSet.createMessage("GqlQuery")
@@ -125,7 +132,7 @@ class DatastoreViewController : NSViewController, NSTextFieldDelegate {
           }
         }
       }
-
+      
     }
   }
 }

+ 109 - 0
Examples/Datastore/Datastore/OAuthClient.swift

@@ -0,0 +1,109 @@
+/*
+ *
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+import Foundation
+
+
+func escape(_ string : String) -> String {
+
+  let dashes = CharacterSet(charactersIn: "-._")
+  let acceptable = dashes.union(.alphanumerics)
+  return string.addingPercentEncoding(withAllowedCharacters: acceptable)!
+}
+
+class OAuthClient {
+  var clientID : String = ""
+  var clientSecret : String = ""
+  var redirectURIs : [String] = []
+  var authURI : String = ""
+  var tokenURI : String = ""
+
+  var token : String? = nil
+
+  init () {
+    clientID = "885917370891-n3r74v6miibn2969estdofr68ggqa1sn.apps.googleusercontent.com"
+    clientSecret = "_JDxU8iGdHYfeeER9AAEaHbn"
+    redirectURIs = ["http://localhost"]
+    authURI  = "https://accounts.google.com/o/oauth2/auth"
+    tokenURI = "https://accounts.google.com/o/oauth2/token"
+  }
+
+  let scope = "https://www.googleapis.com/auth/datastore"
+
+  func authCodeURL(state : String) -> URL? {
+
+    var path = authURI
+    path = path + "?response_type=" + "code"
+    path = path + "&client_id=" + clientID
+    path = path + "&redirect_uri=" + escape(redirectURIs[0])
+    path = path + "&scope=" + escape(scope)
+    path = path + "&state=" + state
+
+    print("PATH " + path)
+
+    return URL(string:path)
+  }
+
+  func exchangeCode(code: String) {
+    let path = tokenURI
+    var body = ""
+    body = body + "client_id=" + clientID
+    body = body + "&client_secret=" + clientSecret
+    body = body + "&code=" + escape(code)
+    body = body + "&grant_type=" + "authorization_code"
+    body = body + "&redirect_uri=" + escape(redirectURIs[0])
+
+    print("path: \(path)")
+    print("body: \(body)")
+
+    let url = URL(string:path)!
+    var request = URLRequest(url:url)
+    request.httpMethod = "POST"
+    request.httpBody = body.data(using:.utf8)
+    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
+
+    print("headers: \(request.allHTTPHeaderFields)")
+    let task = URLSession.shared.dataTask(with:request) { (data, response, error) in
+      var json: [String:Any]!
+      do {
+        json = try JSONSerialization.jsonObject(with:data!, options: JSONSerialization.ReadingOptions()) as? Dictionary
+      } catch {
+        print(error)
+      }
+      print(json)
+      self.token = json["access_token"] as! String?
+      print(self.token)
+
+    }
+    task.resume()
+  }
+}

+ 46 - 0
Examples/Datastore/Datastore/WebViewWindow.xib

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11201" systemVersion="15G31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
+    <dependencies>
+        <deployment identifier="macosx"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11201"/>
+        <plugIn identifier="com.apple.WebKitIBPlugin" version="11201"/>
+    </dependencies>
+    <objects>
+        <customObject id="-2" userLabel="File's Owner" customClass="WebViewWindowController" customModule="Datastore" customModuleProvider="target">
+            <connections>
+                <outlet property="webView" destination="SiX-H8-2jk" id="eix-fS-dNl"/>
+                <outlet property="window" destination="QvC-M9-y7g" id="7Q8-Bm-Iat"/>
+            </connections>
+        </customObject>
+        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
+        <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g">
+            <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
+            <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
+            <rect key="contentRect" x="196" y="240" width="487" height="578"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="1200" height="1897"/>
+            <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
+                <rect key="frame" x="0.0" y="0.0" width="487" height="578"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <webView translatesAutoresizingMaskIntoConstraints="NO" id="SiX-H8-2jk">
+                        <rect key="frame" x="0.0" y="0.0" width="487" height="578"/>
+                        <webPreferences key="preferences" defaultFontSize="16" defaultFixedFontSize="13" minimumFontSize="0">
+                            <nil key="identifier"/>
+                        </webPreferences>
+                    </webView>
+                </subviews>
+                <constraints>
+                    <constraint firstAttribute="bottom" secondItem="SiX-H8-2jk" secondAttribute="bottom" id="6YE-S7-oY3"/>
+                    <constraint firstItem="SiX-H8-2jk" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" id="JVo-V8-E91"/>
+                    <constraint firstAttribute="trailing" secondItem="SiX-H8-2jk" secondAttribute="trailing" id="aOd-yr-GaJ"/>
+                    <constraint firstItem="SiX-H8-2jk" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" id="kgL-mK-4mC"/>
+                </constraints>
+            </view>
+            <connections>
+                <outlet property="delegate" destination="-2" id="dDr-PW-Lxo"/>
+            </connections>
+            <point key="canvasLocation" x="136.5" y="178"/>
+        </window>
+    </objects>
+</document>

+ 95 - 0
Examples/Datastore/Datastore/WebViewWindowController.swift

@@ -0,0 +1,95 @@
+/*
+ *
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+import Cocoa
+import WebKit
+
+class WebViewWindowController : NSWindowController {
+  @IBOutlet var webView : WebView!
+  var url : URL?
+
+  var completion: ((String) -> Void)?
+
+  override func awakeFromNib() {
+    super.awakeFromNib()
+    self.webView.resourceLoadDelegate = self
+    if let url = url {
+      self.webView.mainFrame.load(URLRequest(url:url))
+    }
+  }
+
+}
+
+extension WebViewWindowController : WebResourceLoadDelegate {
+
+  func webView(_ sender: WebView!,
+               resource identifier: Any!,
+               willSend request: URLRequest!,
+               redirectResponse: URLResponse!,
+               from dataSource: WebDataSource!) -> URLRequest! {
+
+    print(request)
+
+    if (request.url!.absoluteString.hasPrefix("http://localhost")) {
+
+      print("incoming \(request.url!.absoluteString)")
+      let keyVals = request.url!.getKeyVals()!
+      let code = keyVals["code"]!
+
+      print("Got it! \(code)")
+
+      if let completion = completion {
+        completion(code)
+      }
+      window!.close()
+      return nil
+    }
+
+    return request
+  }
+}
+
+extension URL {
+  func getKeyVals() -> Dictionary<String, String>? {
+    var results = [String:String]()
+    let keyValues = self.query?.components(separatedBy: "&")
+    if keyValues!.count > 0 {
+      for pair in keyValues! {
+        let kv = pair.components(separatedBy:"=")
+        if kv.count > 1 {
+          results.updateValue(kv[1], forKey: kv[0])
+        }
+      }
+    }
+    return results
+  }
+}