View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  
20  package davmail.exchange.auth;
21  
22  import davmail.BundleMessage;
23  import davmail.Settings;
24  import davmail.ui.tray.DavGatewayTray;
25  import javafx.application.Platform;
26  import javafx.concurrent.Worker;
27  import javafx.embed.swing.JFXPanel;
28  import javafx.scene.Scene;
29  import javafx.scene.control.ProgressBar;
30  import javafx.scene.layout.StackPane;
31  import javafx.scene.web.WebEngine;
32  import javafx.scene.web.WebView;
33  import org.apache.log4j.Logger;
34  import org.w3c.dom.Document;
35  
36  import javax.swing.*;
37  import javax.xml.XMLConstants;
38  import javax.xml.transform.OutputKeys;
39  import javax.xml.transform.Transformer;
40  import javax.xml.transform.TransformerFactory;
41  import javax.xml.transform.dom.DOMSource;
42  import javax.xml.transform.stream.StreamResult;
43  import java.awt.*;
44  import java.awt.event.WindowAdapter;
45  import java.awt.event.WindowEvent;
46  import java.io.ByteArrayOutputStream;
47  import java.io.OutputStreamWriter;
48  import java.net.URL;
49  import java.net.URLConnection;
50  import java.net.URLStreamHandler;
51  import java.nio.charset.StandardCharsets;
52  
53  /**
54   * Interactive authenticator UI based on OpenJFX.
55   * Need access to internal urlhandler on recent JDK versions with: --add-exports java.base/sun.net.www.protocol.https=ALL-UNNAMED
56   */
57  public class O365InteractiveAuthenticatorFrame extends JFrame {
58      private static final Logger LOGGER = Logger.getLogger(O365InteractiveAuthenticatorFrame.class);
59  
60      private O365InteractiveAuthenticator authenticator;
61  
62      static {
63          // register a stream handler for msauth protocol
64          URL.setURLStreamHandlerFactory((String protocol) -> {
65                      if ("msauth".equals(protocol) || "urn".equals(protocol)) {
66                          return new URLStreamHandler() {
67                              @Override
68                              protected URLConnection openConnection(URL u) {
69                                  return new URLConnection(u) {
70                                      @Override
71                                      public void connect() {
72                                          // ignore
73                                      }
74                                  };
75                              }
76                          };
77                      }
78                      return null;
79                  }
80          );
81      }
82  
83      String location;
84      final JFXPanel fxPanel = new JFXPanel();
85  
86      public O365InteractiveAuthenticatorFrame() {
87          setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
88          addWindowListener(new WindowAdapter() {
89              @Override
90              public void windowClosing(WindowEvent e) {
91                  if (!authenticator.isAuthenticated && authenticator.errorCode == null) {
92                      authenticator.errorCode = "user closed authentication window";
93                  }
94              }
95          });
96  
97          setTitle(BundleMessage.format("UI_DAVMAIL_GATEWAY"));
98          try {
99              setIconImages(DavGatewayTray.getFrameIcons());
100         } catch (NoSuchMethodError error) {
101             DavGatewayTray.debug(new BundleMessage("LOG_UNABLE_TO_SET_ICON_IMAGE"));
102         }
103 
104         JPanel mainPanel = new JPanel();
105 
106         mainPanel.add(fxPanel);
107         add(BorderLayout.CENTER, mainPanel);
108 
109         pack();
110         setResizable(true);
111         // center frame
112         setSize(600, 600);
113         setLocationRelativeTo(null);
114         setVisible(true);
115         // bring window to top
116         setAlwaysOnTop(true);
117         setAlwaysOnTop(false);
118     }
119 
120     public void setO365InteractiveAuthenticator(O365InteractiveAuthenticator authenticator) {
121         this.authenticator = authenticator;
122     }
123 
124     private void initFX(final JFXPanel fxPanel, final String url, final String redirectUri) {
125         WebView webView = new WebView();
126         final WebEngine webViewEngine = webView.getEngine();
127 
128         final ProgressBar loadProgress = new ProgressBar();
129         loadProgress.progressProperty().bind(webViewEngine.getLoadWorker().progressProperty());
130 
131         StackPane hBox = new StackPane();
132         hBox.getChildren().setAll(webView, loadProgress);
133         Scene scene = new Scene(hBox);
134         fxPanel.setScene(scene);
135 
136         webViewEngine.setUserAgent(Settings.getUserAgent());
137 
138         webViewEngine.setOnAlert(stringWebEvent -> SwingUtilities.invokeLater(() -> {
139             String message = stringWebEvent.getData();
140             JOptionPane.showMessageDialog(O365InteractiveAuthenticatorFrame.this, message);
141         }));
142         webViewEngine.setOnError(event -> LOGGER.error(event.getMessage()));
143 
144 
145         webViewEngine.getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> {
146             // with Java 15 url with code returns as CANCELLED
147             if (newState == Worker.State.SUCCEEDED || newState == Worker.State.CANCELLED) {
148                 loadProgress.setVisible(false);
149                 location = webViewEngine.getLocation();
150                 updateTitleAndFocus(location);
151                 LOGGER.debug("Webview location: " + location);
152                 // override console.log
153                 O365InteractiveJSLogger.register(webViewEngine);
154                 if (LOGGER.isDebugEnabled()) {
155                     LOGGER.debug(dumpDocument(webViewEngine.getDocument()));
156                 }
157                 if (location.startsWith(redirectUri)) {
158                     LOGGER.debug("Location starts with redirectUri, check code");
159 
160                     authenticator.isAuthenticated = location.contains("code=") && location.contains("&session_state=");
161                     if (!authenticator.isAuthenticated && location.contains("error=")) {
162                         authenticator.errorCode = location.substring(location.indexOf("error="));
163                     }
164                     if (authenticator.isAuthenticated) {
165                         LOGGER.debug("Authenticated location: " + location);
166                         String code = location.substring(location.indexOf("code=") + 5, location.indexOf("&session_state="));
167                         String sessionState = location.substring(location.lastIndexOf('='));
168 
169                         LOGGER.debug("Authentication Code: " + code);
170                         LOGGER.debug("Authentication session state: " + sessionState);
171                         authenticator.code = code;
172                     }
173                     close();
174                 }
175             } else if (newState == Worker.State.FAILED) {
176                 Throwable e = webViewEngine.getLoadWorker().getException();
177                 if (e != null) {
178                     handleError(e);
179                 }
180                 close();
181             } else {
182                 LOGGER.debug(webViewEngine.getLoadWorker().getState() + " " + webViewEngine.getLoadWorker().getMessage() + " " + webViewEngine.getLocation() + " ");
183             }
184 
185         });
186         webViewEngine.load(url);
187     }
188 
189     private void updateTitleAndFocus(final String location) {
190         SwingUtilities.invokeLater(() -> {
191             setState(Frame.NORMAL);
192             setAlwaysOnTop(true);
193             setAlwaysOnTop(false);
194             setTitle("DavMail: " + location);
195         });
196     }
197 
198     public String dumpDocument(Document document) {
199         String result;
200         try {
201             ByteArrayOutputStream baos = new ByteArrayOutputStream();
202             TransformerFactory transformerFactory = TransformerFactory.newInstance();
203             transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
204             transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
205             transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
206             Transformer transformer = transformerFactory.newTransformer();
207             transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
208             transformer.setOutputProperty(OutputKeys.METHOD, "xml");
209             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
210             transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
211             transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
212 
213             transformer.transform(new DOMSource(document),
214                     new StreamResult(new OutputStreamWriter(baos, StandardCharsets.UTF_8)));
215             result = baos.toString("UTF-8");
216         } catch (Exception e) {
217             result = e + " " + e.getMessage();
218         }
219         return result;
220     }
221 
222     public void authenticate(final String initUrl, final String redirectUri) {
223         // Run initFX as JavaFX-Thread
224         Platform.runLater(() -> {
225             try {
226                 Platform.setImplicitExit(false);
227 
228                 initFX(fxPanel, initUrl, redirectUri);
229             } catch (Exception e) {
230                 handleError(e);
231                 close();
232             }
233         });
234     }
235 
236     public void handleError(Throwable t) {
237         LOGGER.error(t + " " + t.getMessage());
238         authenticator.errorCode = t.getMessage();
239         if (authenticator.errorCode == null) {
240             authenticator.errorCode = t.toString();
241         }
242     }
243 
244     public void close() {
245         SwingUtilities.invokeLater(() -> {
246             setVisible(false);
247             dispose();
248         });
249     }
250 
251 }