1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "chrome/browser/ui/cocoa/ssl_client_certificate_selector_cocoa.h"
6
7#import <SecurityInterface/SFChooseIdentityPanel.h>
8
9#include "base/logging.h"
10#include "base/mac/foundation_util.h"
11#include "base/strings/string_util.h"
12#include "base/strings/sys_string_conversions.h"
13#include "base/strings/utf_string_conversions.h"
14#include "chrome/browser/ssl/ssl_client_auth_observer.h"
15#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_mac.h"
16#include "chrome/grit/generated_resources.h"
17#include "content/public/browser/browser_thread.h"
18#include "content/public/browser/web_contents.h"
19#include "net/cert/x509_certificate.h"
20#include "net/cert/x509_util_mac.h"
21#include "net/ssl/ssl_cert_request_info.h"
22#include "ui/base/cocoa/window_size_constants.h"
23#include "ui/base/l10n/l10n_util_mac.h"
24
25using content::BrowserThread;
26
27@interface SFChooseIdentityPanel (SystemPrivate)
28// A system-private interface that dismisses a panel whose sheet was started by
29// -beginSheetForWindow:modalDelegate:didEndSelector:contextInfo:identities:message:
30// as though the user clicked the button identified by returnCode. Verified
31// present in 10.5 through 10.8.
32- (void)_dismissWithCode:(NSInteger)code;
33@end
34
35@interface SSLClientCertificateSelectorCocoa ()
36- (void)onConstrainedWindowClosed;
37@end
38
39class SSLClientAuthObserverCocoaBridge : public SSLClientAuthObserver,
40                                         public ConstrainedWindowMacDelegate {
41 public:
42  SSLClientAuthObserverCocoaBridge(
43      const net::HttpNetworkSession* network_session,
44      net::SSLCertRequestInfo* cert_request_info,
45      const chrome::SelectCertificateCallback& callback,
46      SSLClientCertificateSelectorCocoa* controller)
47      : SSLClientAuthObserver(network_session, cert_request_info, callback),
48        controller_(controller) {
49  }
50
51  // SSLClientAuthObserver implementation:
52  virtual void OnCertSelectedByNotification() OVERRIDE {
53    [controller_ closeWebContentsModalDialog];
54  }
55
56  // ConstrainedWindowMacDelegate implementation:
57  virtual void OnConstrainedWindowClosed(
58      ConstrainedWindowMac* window) OVERRIDE {
59    // |onConstrainedWindowClosed| will delete the sheet which might be still
60    // in use higher up the call stack. Wait for the next cycle of the event
61    // loop to call this function.
62    [controller_ performSelector:@selector(onConstrainedWindowClosed)
63                      withObject:nil
64                      afterDelay:0];
65  }
66
67 private:
68  SSLClientCertificateSelectorCocoa* controller_;  // weak
69};
70
71namespace chrome {
72
73void ShowSSLClientCertificateSelector(
74    content::WebContents* contents,
75    const net::HttpNetworkSession* network_session,
76    net::SSLCertRequestInfo* cert_request_info,
77    const SelectCertificateCallback& callback) {
78  DCHECK_CURRENTLY_ON(BrowserThread::UI);
79  // The dialog manages its own lifetime.
80  SSLClientCertificateSelectorCocoa* selector =
81      [[SSLClientCertificateSelectorCocoa alloc]
82          initWithNetworkSession:network_session
83                 certRequestInfo:cert_request_info
84                        callback:callback];
85  [selector displayForWebContents:contents];
86}
87
88}  // namespace chrome
89
90@implementation SSLClientCertificateSelectorCocoa
91
92- (id)initWithNetworkSession:(const net::HttpNetworkSession*)networkSession
93    certRequestInfo:(net::SSLCertRequestInfo*)certRequestInfo
94           callback:(const chrome::SelectCertificateCallback&)callback {
95  DCHECK(networkSession);
96  DCHECK(certRequestInfo);
97  if ((self = [super init])) {
98    observer_.reset(new SSLClientAuthObserverCocoaBridge(
99        networkSession, certRequestInfo, callback, self));
100  }
101  return self;
102}
103
104- (void)sheetDidEnd:(NSWindow*)parent
105         returnCode:(NSInteger)returnCode
106            context:(void*)context {
107  net::X509Certificate* cert = NULL;
108  if (returnCode == NSFileHandlingPanelOKButton) {
109    CFRange range = CFRangeMake(0, CFArrayGetCount(identities_));
110    CFIndex index =
111        CFArrayGetFirstIndexOfValue(identities_, range, [panel_ identity]);
112    if (index != -1)
113      cert = certificates_[index].get();
114    else
115      NOTREACHED();
116  }
117
118  // Finally, tell the backend which identity (or none) the user selected.
119  observer_->StopObserving();
120  observer_->CertificateSelected(cert);
121
122  if (!closePending_)
123    constrainedWindow_->CloseWebContentsModalDialog();
124}
125
126- (void)displayForWebContents:(content::WebContents*)webContents {
127  // Create an array of CFIdentityRefs for the certificates:
128  size_t numCerts = observer_->cert_request_info()->client_certs.size();
129  identities_.reset(CFArrayCreateMutable(
130      kCFAllocatorDefault, numCerts, &kCFTypeArrayCallBacks));
131  for (size_t i = 0; i < numCerts; ++i) {
132    SecCertificateRef cert =
133        observer_->cert_request_info()->client_certs[i]->os_cert_handle();
134    SecIdentityRef identity;
135    if (SecIdentityCreateWithCertificate(NULL, cert, &identity) == noErr) {
136      CFArrayAppendValue(identities_, identity);
137      CFRelease(identity);
138      certificates_.push_back(observer_->cert_request_info()->client_certs[i]);
139    }
140  }
141
142  // Get the message to display:
143  NSString* message = l10n_util::GetNSStringF(
144      IDS_CLIENT_CERT_DIALOG_TEXT,
145      base::ASCIIToUTF16(
146          observer_->cert_request_info()->host_and_port.ToString()));
147
148  // Create and set up a system choose-identity panel.
149  panel_.reset([[SFChooseIdentityPanel alloc] init]);
150  [panel_ setInformativeText:message];
151  [panel_ setDefaultButtonTitle:l10n_util::GetNSString(IDS_OK)];
152  [panel_ setAlternateButtonTitle:l10n_util::GetNSString(IDS_CANCEL)];
153  SecPolicyRef sslPolicy;
154  if (net::x509_util::CreateSSLClientPolicy(&sslPolicy) == noErr) {
155    [panel_ setPolicies:(id)sslPolicy];
156    CFRelease(sslPolicy);
157  }
158
159  constrainedWindow_.reset(
160      new ConstrainedWindowMac(observer_.get(), webContents, self));
161  observer_->StartObserving();
162}
163
164- (void)closeWebContentsModalDialog {
165  DCHECK(constrainedWindow_);
166  constrainedWindow_->CloseWebContentsModalDialog();
167}
168
169- (NSWindow*)overlayWindow {
170  return overlayWindow_;
171}
172
173- (SFChooseIdentityPanel*)panel {
174  return panel_;
175}
176
177- (void)showSheetForWindow:(NSWindow*)window {
178  NSString* title = l10n_util::GetNSString(IDS_CLIENT_CERT_DIALOG_TITLE);
179  overlayWindow_.reset([window retain]);
180  [panel_ beginSheetForWindow:window
181                modalDelegate:self
182               didEndSelector:@selector(sheetDidEnd:returnCode:context:)
183                  contextInfo:NULL
184                   identities:base::mac::CFToNSCast(identities_)
185                      message:title];
186}
187
188- (void)closeSheetWithAnimation:(BOOL)withAnimation {
189  closePending_ = YES;
190  overlayWindow_.reset();
191  // Closing the sheet using -[NSApp endSheet:] doesn't work so use the private
192  // method.
193  [panel_ _dismissWithCode:NSFileHandlingPanelCancelButton];
194}
195
196- (void)hideSheet {
197  NSWindow* sheetWindow = [overlayWindow_ attachedSheet];
198  [sheetWindow setAlphaValue:0.0];
199
200  oldResizesSubviews_ = [[sheetWindow contentView] autoresizesSubviews];
201  [[sheetWindow contentView] setAutoresizesSubviews:NO];
202
203  oldSheetFrame_ = [sheetWindow frame];
204  NSRect overlayFrame = [overlayWindow_ frame];
205  oldSheetFrame_.origin.x -= NSMinX(overlayFrame);
206  oldSheetFrame_.origin.y -= NSMinY(overlayFrame);
207  [sheetWindow setFrame:ui::kWindowSizeDeterminedLater display:NO];
208}
209
210- (void)unhideSheet {
211  NSWindow* sheetWindow = [overlayWindow_ attachedSheet];
212  NSRect overlayFrame = [overlayWindow_ frame];
213  oldSheetFrame_.origin.x += NSMinX(overlayFrame);
214  oldSheetFrame_.origin.y += NSMinY(overlayFrame);
215  [sheetWindow setFrame:oldSheetFrame_ display:NO];
216  [[sheetWindow contentView] setAutoresizesSubviews:oldResizesSubviews_];
217  [[overlayWindow_ attachedSheet] setAlphaValue:1.0];
218}
219
220- (void)pulseSheet {
221  // NOOP
222}
223
224- (void)makeSheetKeyAndOrderFront {
225  [[overlayWindow_ attachedSheet] makeKeyAndOrderFront:nil];
226}
227
228- (void)updateSheetPosition {
229  // NOOP
230}
231
232- (void)onConstrainedWindowClosed {
233  observer_->StopObserving();
234  panel_.reset();
235  constrainedWindow_.reset();
236  [self release];
237}
238
239@end
240