xref: /trunk/main/swext/mediawiki/src/com/sun/star/wiki/WikiSession.java (revision 33efb51f76a78baf5a276c335781687d66cf274a)
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