1 /************************************************************** 2 * 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, 14 * software distributed under the License is distributed on an 15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 * KIND, either express or implied. See the License for the 17 * specific language governing permissions and limitations 18 * under the License. 19 * 20 *************************************************************/ 21 22 23 24 package com.sun.star.wiki; 25 26 import com.sun.star.uno.XComponentContext; 27 import java.io.ByteArrayInputStream; 28 import java.io.ByteArrayOutputStream; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.io.UnsupportedEncodingException; 33 import java.net.CookieManager; 34 import java.net.CookiePolicy; 35 import java.net.HttpURLConnection; 36 import java.net.Proxy; 37 import java.net.URISyntaxException; 38 import java.net.URL; 39 import java.net.URLEncoder; 40 import java.security.KeyStore; 41 import java.security.cert.CertificateException; 42 import java.security.cert.X509Certificate; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import javax.net.ssl.HttpsURLConnection; 47 import javax.net.ssl.SSLContext; 48 import javax.net.ssl.SSLSocketFactory; 49 import javax.net.ssl.TrustManager; 50 import javax.net.ssl.TrustManagerFactory; 51 import javax.net.ssl.X509TrustManager; 52 import javax.xml.parsers.DocumentBuilder; 53 import javax.xml.parsers.DocumentBuilderFactory; 54 import org.w3c.dom.Document; 55 import org.w3c.dom.Element; 56 import org.w3c.dom.NodeList; 57 58 /** An HTTP session with one MediaWiki server, talking to its Action API (api.php). */ 59 public class WikiSession 60 { 61 public static class Response 62 { 63 public int m_nStatus; 64 public String m_sBody; 65 public URL m_aURL; 66 } 67 68 private static final int MAX_REDIRECTS = 10; 69 private static final int CONNECT_TIMEOUT_MS = 30000; 70 private static final String USER_AGENT = "Apache-OpenOffice-Wiki-Publisher"; 71 72 private static SSLSocketFactory m_aUnknownCertSocketFactory; 73 74 private XComponentContext m_xContext; 75 private URL m_aMainURL; 76 // not CookieHandler.setDefault(): that would be shared by everything in the office JVM 77 private CookieManager m_aCookies = new CookieManager( null, CookiePolicy.ACCEPT_ORIGINAL_SERVER ); 78 WikiSession( XComponentContext xContext, URL aMainURL )79 public WikiSession( XComponentContext xContext, URL aMainURL ) 80 { 81 m_xContext = xContext; 82 m_aMainURL = aMainURL; 83 } 84 GetMainURL()85 public URL GetMainURL() 86 { 87 return m_aMainURL; 88 } 89 Get( URL aURL )90 public Response Get( URL aURL ) 91 throws IOException, WikiCancelException 92 { 93 for ( int nRedirect = 0; ; nRedirect++ ) 94 { 95 HttpURLConnection aConn = Open( aURL ); 96 Response aResponse = Execute( aConn, aURL, null ); 97 int nStatus = aResponse.m_nStatus; 98 String sLocation = aConn.getHeaderField( "Location" ); 99 if ( nStatus < 300 || nStatus > 399 || sLocation == null || nRedirect >= MAX_REDIRECTS ) 100 return aResponse; 101 102 aURL = new URL( aURL, sLocation ); 103 } 104 } 105 Post( URL aURL, String[][] pParams )106 public Response Post( URL aURL, String[][] pParams ) 107 throws IOException, WikiCancelException 108 { 109 StringBuilder aBody = new StringBuilder(); 110 for ( int nInd = 0; nInd < pParams.length; nInd++ ) 111 { 112 if ( pParams[nInd] == null || pParams[nInd][0] == null || pParams[nInd][1] == null ) 113 continue; 114 if ( aBody.length() > 0 ) 115 aBody.append( '&' ); 116 aBody.append( URLEncoder.encode( pParams[nInd][0], "UTF-8" ) ); 117 aBody.append( '=' ); 118 aBody.append( URLEncoder.encode( pParams[nInd][1], "UTF-8" ) ); 119 } 120 121 HttpURLConnection aConn = Open( aURL ); 122 aConn.setRequestMethod( "POST" ); 123 aConn.setDoOutput( true ); 124 aConn.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded; charset=UTF-8" ); 125 return Execute( aConn, aURL, aBody.toString().getBytes( "UTF-8" ) ); 126 } 127 128 /** Calls api.php; HTTP errors and non-XML answers throw, API-level errors are left in the returned document. */ CallAPI( String[][] pParams )129 public Document CallAPI( String[][] pParams ) 130 throws IOException, WikiCancelException 131 { 132 String[][] pAllParams = new String[pParams.length + 1][]; 133 pAllParams[0] = new String[] { "format", "xml" }; 134 System.arraycopy( pParams, 0, pAllParams, 1, pParams.length ); 135 136 Response aResponse = Post( new URL( m_aMainURL, "api.php" ), pAllParams ); 137 if ( aResponse.m_nStatus != 200 ) 138 throw new IOException( "MediaWiki API request failed with HTTP status " + aResponse.m_nStatus ); 139 140 try 141 { 142 DocumentBuilderFactory aFactory = DocumentBuilderFactory.newInstance(); 143 aFactory.setFeature( "http://apache.org/xml/features/disallow-doctype-decl", true ); 144 aFactory.setExpandEntityReferences( false ); 145 DocumentBuilder aBuilder = aFactory.newDocumentBuilder(); 146 Document aDoc = aBuilder.parse( new ByteArrayInputStream( aResponse.m_sBody.getBytes( "UTF-8" ) ) ); 147 if ( !"api".equals( aDoc.getDocumentElement().getTagName() ) ) 148 throw new IOException( "Not a MediaWiki API response" ); 149 return aDoc; 150 } 151 catch ( IOException e ) 152 { 153 throw e; 154 } 155 catch ( Exception e ) 156 { 157 IOException aIOEx = new IOException( "Not a MediaWiki API response" ); 158 aIOEx.initCause( e ); 159 throw aIOEx; 160 } 161 } 162 163 /** Returns the requested token ("login", "csrf"), or null if the wiki does not provide it. */ GetToken( String sType )164 public String GetToken( String sType ) 165 throws IOException, WikiCancelException 166 { 167 Document aDoc = CallAPI( new String[][] { { "action", "query" }, { "meta", "tokens" }, { "type", sType } } ); 168 Element aTokens = GetElement( aDoc, "tokens" ); 169 if ( aTokens == null || !aTokens.hasAttribute( sType + "token" ) ) 170 return null; 171 return aTokens.getAttribute( sType + "token" ); 172 } 173 GetElement( Document aDoc, String sTagName )174 public static Element GetElement( Document aDoc, String sTagName ) 175 { 176 NodeList aList = aDoc.getElementsByTagName( sTagName ); 177 return aList.getLength() > 0 ? (Element) aList.item( 0 ) : null; 178 } 179 Open( URL aURL )180 private HttpURLConnection Open( URL aURL ) 181 throws IOException, WikiCancelException 182 { 183 if ( !Helper.IsConnectionAllowed() ) 184 throw new WikiCancelException(); 185 186 String sProtocol = aURL.getProtocol(); 187 if ( !sProtocol.equals( "http" ) && !sProtocol.equals( "https" ) ) 188 throw new IOException( "Unsupported protocol: " + sProtocol ); 189 190 Proxy aProxy = Helper.GetProxy( m_xContext, aURL ); 191 HttpURLConnection aConn = (HttpURLConnection) ( aProxy == null ? aURL.openConnection() : aURL.openConnection( aProxy ) ); 192 aConn.setInstanceFollowRedirects( false ); 193 aConn.setConnectTimeout( CONNECT_TIMEOUT_MS ); 194 aConn.setUseCaches( false ); 195 aConn.setRequestProperty( "User-Agent", USER_AGENT ); 196 197 if ( aConn instanceof HttpsURLConnection && Helper.AllowUnknownCert( m_xContext, aURL.getHost() ) ) 198 ( (HttpsURLConnection) aConn ).setSSLSocketFactory( GetUnknownCertSocketFactory() ); 199 200 try 201 { 202 Map<String, List<String>> aCookieHeaders = m_aCookies.get( aURL.toURI(), new HashMap<String, List<String>>() ); 203 List<String> aCookies = aCookieHeaders.get( "Cookie" ); 204 if ( aCookies != null && !aCookies.isEmpty() ) 205 { 206 StringBuilder aCookie = new StringBuilder(); 207 for ( String sCookie : aCookies ) 208 { 209 if ( aCookie.length() > 0 ) 210 aCookie.append( "; " ); 211 aCookie.append( sCookie ); 212 } 213 aConn.setRequestProperty( "Cookie", aCookie.toString() ); 214 } 215 } 216 catch ( URISyntaxException e ) 217 { 218 throw new IOException( e.getMessage() ); 219 } 220 221 return aConn; 222 } 223 Execute( HttpURLConnection aConn, URL aURL, byte[] pBody )224 private Response Execute( HttpURLConnection aConn, URL aURL, byte[] pBody ) 225 throws IOException, WikiCancelException 226 { 227 Helper.RegisterConnection( aConn ); 228 try 229 { 230 // disconnect() is a no-op until the TCP connect has finished, so recheck a cancel that came during it 231 aConn.connect(); 232 if ( !Helper.IsConnectionAllowed() ) 233 throw new WikiCancelException(); 234 235 if ( pBody != null ) 236 { 237 OutputStream aOut = aConn.getOutputStream(); 238 try 239 { 240 aOut.write( pBody ); 241 } 242 finally 243 { 244 aOut.close(); 245 } 246 } 247 248 Response aResponse = new Response(); 249 aResponse.m_nStatus = aConn.getResponseCode(); 250 aResponse.m_aURL = aURL; 251 252 try 253 { 254 m_aCookies.put( aURL.toURI(), aConn.getHeaderFields() ); 255 } 256 catch ( URISyntaxException e ) 257 { 258 throw new IOException( e.getMessage() ); 259 } 260 261 InputStream aIn = aResponse.m_nStatus >= 400 ? aConn.getErrorStream() : aConn.getInputStream(); 262 aResponse.m_sBody = ( aIn == null ) ? "" : ReadAll( aIn, GetCharset( aConn.getContentType() ) ); 263 return aResponse; 264 } 265 catch ( IOException e ) 266 { 267 if ( !Helper.IsConnectionAllowed() ) 268 throw new WikiCancelException(); 269 throw e; 270 } 271 finally 272 { 273 Helper.UnregisterConnection( aConn ); 274 } 275 } 276 GetCharset( String sContentType )277 private static String GetCharset( String sContentType ) 278 { 279 if ( sContentType != null ) 280 { 281 String[] pParts = sContentType.split( ";" ); 282 for ( int nInd = 1; nInd < pParts.length; nInd++ ) 283 { 284 String sPart = pParts[nInd].trim(); 285 if ( sPart.regionMatches( true, 0, "charset=", 0, 8 ) ) 286 return sPart.substring( 8 ).replace( "\"", "" ).trim(); 287 } 288 } 289 return "UTF-8"; 290 } 291 ReadAll( InputStream aIn, String sCharset )292 private static String ReadAll( InputStream aIn, String sCharset ) 293 throws IOException 294 { 295 try 296 { 297 ByteArrayOutputStream aOut = new ByteArrayOutputStream(); 298 byte[] pBuf = new byte[8192]; 299 int nRead; 300 while ( ( nRead = aIn.read( pBuf ) ) != -1 ) 301 aOut.write( pBuf, 0, nRead ); 302 try 303 { 304 return aOut.toString( sCharset ); 305 } 306 catch ( UnsupportedEncodingException e ) 307 { 308 return aOut.toString( "UTF-8" ); 309 } 310 } 311 finally 312 { 313 aIn.close(); 314 } 315 } 316 GetUnknownCertSocketFactory()317 private static synchronized SSLSocketFactory GetUnknownCertSocketFactory() 318 throws IOException 319 { 320 if ( m_aUnknownCertSocketFactory == null ) 321 { 322 try 323 { 324 TrustManagerFactory aFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ); 325 aFactory.init( (KeyStore) null ); 326 X509TrustManager aDefault = null; 327 TrustManager[] pManagers = aFactory.getTrustManagers(); 328 for ( int nInd = 0; nInd < pManagers.length && aDefault == null; nInd++ ) 329 if ( pManagers[nInd] instanceof X509TrustManager ) 330 aDefault = (X509TrustManager) pManagers[nInd]; 331 if ( aDefault == null ) 332 throw new IOException( "No default trust manager" ); 333 334 SSLContext aContext = SSLContext.getInstance( "TLS" ); 335 aContext.init( null, new TrustManager[] { new UnknownCertTrustManager( aDefault ) }, null ); 336 m_aUnknownCertSocketFactory = aContext.getSocketFactory(); 337 } 338 catch ( IOException e ) 339 { 340 throw e; 341 } 342 catch ( Exception e ) 343 { 344 IOException aIOEx = new IOException( "Cannot initialize TLS" ); 345 aIOEx.initCause( e ); 346 throw aIOEx; 347 } 348 } 349 350 return m_aUnknownCertSocketFactory; 351 } 352 353 /** For servers the configuration marks with AllowUnknownCertificate: a server certificate 354 that does not chain to a trusted root is accepted if it is within its validity period. 355 The host name is still verified by HttpsURLConnection. */ 356 private static class UnknownCertTrustManager implements X509TrustManager 357 { 358 private X509TrustManager m_aDefault; 359 UnknownCertTrustManager( X509TrustManager aDefault )360 UnknownCertTrustManager( X509TrustManager aDefault ) 361 { 362 m_aDefault = aDefault; 363 } 364 getAcceptedIssuers()365 public X509Certificate[] getAcceptedIssuers() 366 { 367 return m_aDefault.getAcceptedIssuers(); 368 } 369 checkClientTrusted( X509Certificate[] pCerts, String sAuthType )370 public void checkClientTrusted( X509Certificate[] pCerts, String sAuthType ) throws CertificateException 371 { 372 m_aDefault.checkClientTrusted( pCerts, sAuthType ); 373 } 374 checkServerTrusted( X509Certificate[] pCerts, String sAuthType )375 public void checkServerTrusted( X509Certificate[] pCerts, String sAuthType ) throws CertificateException 376 { 377 try 378 { 379 m_aDefault.checkServerTrusted( pCerts, sAuthType ); 380 } 381 catch ( CertificateException e ) 382 { 383 if ( pCerts == null || pCerts.length == 0 ) 384 throw e; 385 pCerts[0].checkValidity(); 386 } 387 } 388 } 389 } 390