001/** 
002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net>
003 * 
004 * This library is free software; you can redistribute it and/or
005 * modify it under the terms of the GNU Lesser General Public
006 * License as published by the Free Software Foundation; either
007 * version 2.1 of the License, or (at your option) any later version.
008 * 
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
012 * Lesser General Public License for more details.
013 * 
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this library; if not, write to the Free Software
016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017 */
018
019package net.sourceforge.spnego;
020
021import java.io.ByteArrayOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.net.HttpURLConnection;
026import java.net.URL;
027import java.security.PrivilegedActionException;
028import java.util.ArrayList;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.concurrent.locks.Lock;
034import java.util.concurrent.locks.ReentrantLock;
035import java.util.logging.Level;
036import java.util.logging.Logger;
037
038import javax.security.auth.callback.CallbackHandler;
039import javax.security.auth.login.LoginContext;
040import javax.security.auth.login.LoginException;
041
042import net.sourceforge.spnego.SpnegoHttpFilter.Constants;
043
044import org.ietf.jgss.GSSContext;
045import org.ietf.jgss.GSSCredential;
046import org.ietf.jgss.GSSException;
047
048/**
049 * This Class may be used by custom clients as a convenience when connecting 
050 * to a protected HTTP server.
051 * 
052 * <p>
053 * This mechanism is an alternative to HTTP Basic Authentication where the 
054 * HTTP server does not support Basic Auth but instead has SPNEGO support 
055 * (take a look at {@link SpnegoHttpFilter}).
056 * </p>
057 * 
058 * <p>
059 * A krb5.conf and a login.conf is required when using this class. Take a 
060 * look at the <a href="http://spnego.sourceforge.net" target="_blank">spnego.sourceforge.net</a> 
061 * documentation for an example krb5.conf and login.conf file. 
062 * Also, you must provide a keytab file, or a username and password, or allowtgtsessionkey.
063 * </p>
064 * 
065 * <p>
066 * Example usage (username/password):
067 * <pre>
068 *     public static void main(final String[] args) throws Exception {
069 *         System.setProperty("java.security.krb5.conf", "krb5.conf");
070 *         System.setProperty("sun.security.krb5.debug", "true");
071 *         System.setProperty("java.security.auth.login.config", "login.conf");
072 *         
073 *         SpnegoHttpURLConnection spnego = null;
074 *         
075 *         try {
076 *             spnego = new SpnegoHttpURLConnection("spnego-client", "dfelix", "myp@s5");
077 *             spnego.connect(new URL("http://medusa:8080/index.jsp"));
078 *             
079 *             System.out.println(spnego.getResponseCode());
080 *         
081 *         } finally {
082 *             if (null != spnego) {
083 *                 spnego.disconnect();
084 *             }
085 *         }
086 *     }
087 * </pre>
088 * </p>
089 * 
090 * <p>
091 * Alternatively, if the server supports HTTP Basic Authentication, this Class 
092 * is NOT needed and instead you can do something like the following:
093 * <pre>
094 *     public static void main(final String[] args) throws Exception {
095 *         final String creds = "dfelix:myp@s5";
096 *         
097 *         final String token = Base64.encode(creds.getBytes());
098 *         
099 *         URL url = new URL("http://medusa:8080/index.jsp");
100 *         
101 *         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
102 *         
103 *         conn.setRequestProperty(Constants.AUTHZ_HEADER
104 *                 , Constants.BASIC_HEADER + " " + token);
105 *                 
106 *         conn.connect();
107 *         
108 *         System.out.println("Response Code:" + conn.getResponseCode());
109 *     }
110 * </pre>
111 * </p>
112 * 
113 * <p>
114 * To see a working example and instructions on how to use a keytab, take 
115 * a look at the <a href="http://spnego.sourceforge.net/client_keytab.html"
116 * target="_blank">creating a client keytab</a> example.
117 * </p>
118 * 
119 * <p>
120 * Finally, the {@link SpnegoSOAPConnection} class is another example of a class 
121 * that uses this class.
122 * <p>
123 * 
124 * @author Darwin V. Felix
125 * 
126 */
127public final class SpnegoHttpURLConnection {
128
129    private static final Logger LOGGER = Logger.getLogger(Constants.LOGGER_NAME);
130    
131    /** GSSContext is not thread-safe. */
132    private static final Lock LOCK = new ReentrantLock();
133    
134    private static final byte[] EMPTY_BYTE = new byte[0];
135
136    /**
137     * If false, this connection object has not created a communications link to 
138     * the specified URL. If true, the communications link has been established.
139     */
140    private transient boolean connected = false;
141
142    /**
143     * Default is set to false instead of true.
144     * 
145     * @see java.net.HttpURLConnection#getInstanceFollowRedirects()
146     */
147    private boolean instanceFollowRedirects = true;
148
149    /**
150     * Default is GET.
151     * 
152     * @see java.net.HttpURLConnection#getRequestMethod()
153     */
154    private transient String requestMethod = "GET";
155    
156    /**
157     * @see java.net.URLConnection#getRequestProperties()
158     */
159    private final transient Map<String, List<String>> requestProperties = 
160        new LinkedHashMap<String, List<String>>();
161
162    /** 
163     * Login Context for authenticating client. If username/password 
164     * or GSSCredential is provided (in constructor) then this 
165     * field will always be null.
166     */
167    private final transient LoginContext loginContext;
168
169    /**
170     * Client's credentials. If username/password or LoginContext is provided 
171     * (in constructor) then this field will always be null.
172     */
173    private transient GSSCredential credential;
174
175    /** 
176     * Flag to determine if GSSContext has been established. Users of this 
177     * class should always check that this field is true before using/trusting 
178     * the contents of the response.
179     */
180    private transient boolean cntxtEstablished = false;
181
182    /** 
183     * Ref to HTTP URL Connection object after calling connect method. 
184     * Always call spnego.disconnect() when done using this class.
185     */
186    private transient HttpURLConnection conn = null;
187
188    /** 
189     * Request credential to be delegated. 
190     * Default is false. 
191     */
192    private transient boolean reqCredDeleg = false;
193    
194    /**
195     * Determines if the GSSCredentials (if any) used during the 
196     * connection request should be automatically disposed by 
197     * this class when finished.
198     * Default is true.
199     */
200    private transient boolean autoDisposeCreds = true;
201    
202    /**
203     * Number of times request was redirected.
204     */
205    private transient int redirectCount = 0;
206
207    /**
208     * GSSContext request Mutual Authentication.
209     */
210    private transient boolean mutualAuth = true;
211    
212    /**
213     * GSSContext request Message Confidentiality.
214     * Default is true.
215     */
216    private transient boolean confidentiality = true;
217    
218    /**
219     * GSSContext request Message Integrity.
220     * Default is true.
221     */
222    private transient boolean messageIntegrity = true;
223    
224    /**
225     * GSSContext request Replay Detection.
226     * Default is true.
227     */
228    private transient boolean replayDetection = true;
229    
230    /**
231     * GSSContext request Sequence Detection.
232     * Default is true.
233     */
234    private transient boolean sequenceDetection = true;
235    
236    /**
237     * Number of times redirects will be allowed.
238     */
239    private static final int MAX_REDIRECTS = 20;
240
241    /**
242     * Creates an instance where the LoginContext relies on a keytab 
243     * file being specified by "java.security.auth.login.config" or 
244     * where LoginContext relies on tgtsessionkey.
245     * 
246     * @param loginModuleName 
247     * @throws LoginException 
248     */
249    public SpnegoHttpURLConnection(final String loginModuleName)
250        throws LoginException {
251
252        this.loginContext = new LoginContext(loginModuleName);
253        this.loginContext.login();
254        this.credential = null;
255    }
256    
257    /**
258     * Create an instance where the GSSCredential is specified by the parameter 
259     * and where the GSSCredential is automatically disposed after use.
260     *  
261     * @param creds credentials to use
262     */
263    public SpnegoHttpURLConnection(final GSSCredential creds) {
264        this(creds, true);
265    }
266    
267    /**
268     * Create an instance where the GSSCredential is specified by the parameter 
269     * and whether the GSSCredential should be disposed after use.
270     * 
271     * @param creds credentials to use
272     * @param dispose true if GSSCredential should be diposed after use
273     */
274    public SpnegoHttpURLConnection(final GSSCredential creds, final boolean dispose) {
275        this.loginContext = null;
276        this.credential = creds;
277        this.autoDisposeCreds = dispose;
278    }
279
280    /**
281     * Creates an instance where the LoginContext does not require a keytab
282     * file. However, the "java.security.auth.login.config" property must still
283     * be set prior to instantiating this object.
284     * 
285     * @param loginModuleName 
286     * @param username 
287     * @param password 
288     * @throws LoginException 
289     */
290    public SpnegoHttpURLConnection(final String loginModuleName,
291        final String username, final String password) throws LoginException {
292
293        final CallbackHandler handler = SpnegoProvider.getUsernamePasswordHandler(
294                username, password);
295
296        this.loginContext = new LoginContext(loginModuleName, handler);
297        this.loginContext.login();
298        this.credential = null;
299    }
300
301    /**
302     * Throws IllegalStateException if this connection object has not yet created 
303     * a communications link to the specified URL.
304     */
305    private void assertConnected() {
306        if (!this.connected) {
307            throw new IllegalStateException("Not connected.");
308        }
309    }
310
311    /**
312     * Throws IllegalStateException if this connection object has already created 
313     * a communications link to the specified URL.
314     */
315    private void assertNotConnected() {
316        if (this.connected) {
317            throw new IllegalStateException("Already connected.");
318        }
319    }
320
321    /**
322     * Opens a communications link to the resource referenced by 
323     * this URL, if such a connection has not already been established.
324     * 
325     * <p>
326     * This implementation simply calls this objects 
327     * connect(URL, ByteArrayOutputStream) method but passing in a null 
328     * for the second argument.
329     * </p>
330     * 
331     * @param url 
332     * @return an HttpURLConnection object
333     * @throws GSSException 
334     * @throws PrivilegedActionException 
335     * @throws IOException 
336     * @throws LoginException 
337     * 
338     * @see java.net.URLConnection#connect()
339     */
340    public HttpURLConnection connect(final URL url)
341        throws GSSException, PrivilegedActionException, IOException {
342        
343        return this.connect(url, null);
344    }
345
346    /**
347     * Opens a communications link to the resource referenced by 
348     * this URL, if such a connection has not already been established.
349     * 
350     * @param url 
351     * @param dooutput optional message/payload to send to server
352     * @return an HttpURLConnection object
353     * @throws GSSException 
354     * @throws PrivilegedActionException 
355     * @throws IOException 
356     * @throws LoginException 
357     * 
358     * @see java.net.URLConnection#connect()
359     */
360    public HttpURLConnection connect(final URL url, final ByteArrayOutputStream dooutput)
361        throws GSSException, PrivilegedActionException, IOException {
362        
363        // assert
364        if (!this.messageIntegrity && this.confidentiality) {
365            throw new IllegalStateException("Message Integrity was set "
366                    + "to false but Confidentiality set to true.");
367        }
368
369        assertNotConnected();
370
371        GSSContext context = null;
372        
373        try {
374            byte[] data = null;
375            
376            SpnegoHttpURLConnection.LOCK.lock();
377            try {
378                // work-around to GSSContext/AD timestamp vs sequence field replay bug
379                try { Thread.sleep(31); } catch (InterruptedException e) { assert true; }
380                
381                context = this.getGSSContext(url);
382                context.requestMutualAuth(this.mutualAuth);
383                context.requestConf(this.confidentiality);
384                context.requestInteg(this.messageIntegrity);
385                context.requestReplayDet(this.replayDetection);
386                context.requestSequenceDet(this.sequenceDetection);
387                context.requestCredDeleg(this.reqCredDeleg);
388                
389                data = context.initSecContext(EMPTY_BYTE, 0, 0);
390            } finally {
391                SpnegoHttpURLConnection.LOCK.unlock();
392            }
393            
394            this.conn = (HttpURLConnection) url.openConnection();
395            this.connected = true;
396
397            final Set<String> keys = this.requestProperties.keySet();
398            for (final String key : keys) {
399                for (String value : this.requestProperties.get(key)) {
400                    this.conn.addRequestProperty(key, value);
401                }
402            }
403
404            this.conn.setInstanceFollowRedirects(false);
405            this.conn.setRequestMethod(this.requestMethod);
406
407            this.conn.setRequestProperty(Constants.AUTHZ_HEADER
408                , Constants.NEGOTIATE_HEADER + ' ' + Base64.encode(data));
409
410            if (null != dooutput && dooutput.size() > 0) {
411                this.conn.setDoOutput(true);
412                dooutput.writeTo(this.conn.getOutputStream());
413            }
414
415            this.conn.connect();
416            
417            // redirect if 302
418            if (this.conn.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP 
419                    && this.redirectCount < SpnegoHttpURLConnection.MAX_REDIRECTS) {
420                
421                return this.redirect(url, dooutput);
422            }
423
424            final SpnegoAuthScheme scheme = SpnegoProvider.getAuthScheme(
425                    this.conn.getHeaderField(Constants.AUTHN_HEADER));
426            
427            // app servers will not return a WWW-Authenticate on 302, (and 30x...?)
428            if (null == scheme) {
429                LOGGER.fine("SpnegoProvider.getAuthScheme(...) returned null.");
430                
431            // client requesting to skip context loop if 200 and mutualAuth=false
432            } else if (this.conn.getResponseCode() == HttpURLConnection.HTTP_OK
433                    && !this.mutualAuth) {
434                LOGGER.fine("SpnegoProvider.getAuthScheme(...) returned null.");
435                
436            } else {
437                data = scheme.getToken();
438    
439                if (Constants.NEGOTIATE_HEADER.equalsIgnoreCase(scheme.getScheme())) {
440                    SpnegoHttpURLConnection.LOCK.lock();
441                    try {
442                        data = context.initSecContext(data, 0, data.length);
443                    } finally {
444                        SpnegoHttpURLConnection.LOCK.unlock();
445                    }
446
447                    // TODO : support context loops where i>1
448                    if (null != data) {
449                        LOGGER.warning("Server requested context loop: " + data.length);
450                    }
451                    
452                } else {
453                    throw new UnsupportedOperationException("Scheme NOT Supported: " 
454                            + scheme.getScheme());
455                }
456
457                this.cntxtEstablished = context.isEstablished();
458            }
459        } finally {
460            this.dispose(context);
461        }
462
463        return this.conn;
464    }
465
466    /**
467     * Logout the LoginContext instance, and call dispose() on GSSCredential 
468     * if autoDisposeCreds is set to true, and call dispose on the passed-in 
469     * GSSContext instance.
470     */
471    private void dispose(final GSSContext context) {
472        if (null != context) {
473            try {
474                SpnegoHttpURLConnection.LOCK.lock();
475                try {
476                    context.dispose();
477                } finally {
478                    SpnegoHttpURLConnection.LOCK.unlock();
479                }
480            } catch (GSSException gsse) {
481                LOGGER.log(Level.WARNING, "call to dispose context failed.", gsse);
482            }
483        }
484        
485        if (null != this.credential && this.autoDisposeCreds) {
486            try {
487                this.credential.dispose();
488            } catch (final GSSException gsse) {
489                LOGGER.log(Level.WARNING, "call to dispose credential failed.", gsse);
490            }
491        }
492        
493        if (null != this.loginContext) {
494            try {
495                this.loginContext.logout();
496            } catch (final LoginException lex) {
497                LOGGER.log(Level.WARNING, "call to logout context failed.", lex);
498            }
499        }
500    }
501
502    /**
503     * Logout and clear request properties.
504     * 
505     * @see java.net.HttpURLConnection#disconnect()
506     */
507    public void disconnect() {
508        this.dispose(null);
509        this.requestProperties.clear();
510        this.connected = false;
511        if (null != this.conn) {
512            this.conn.disconnect();
513        }
514    }
515
516    /**
517     * Returns true if GSSContext has been established.
518     * 
519     * @return true if GSSContext has been established, false otherwise.
520     */
521    public boolean isContextEstablished() {
522        return this.cntxtEstablished;
523    }
524
525    /**
526     * Internal sanity check to validate not null key/value pairs.
527     */
528    private void assertKeyValue(final String key, final String value) {
529        if (null == key || key.isEmpty()) {
530            throw new IllegalArgumentException("key parameter is null or empty");
531        }
532        if (null == value) {
533            throw new IllegalArgumentException("value parameter is null");
534        }
535    }
536
537    /**
538     * Adds an HTTP Request property.
539     * 
540     * @param key request property name
541     * @param value request propery value
542     * @see java.net.URLConnection#addRequestProperty(String, String)
543     */
544    public void addRequestProperty(final String key, final String value) {
545        assertNotConnected();
546        assertKeyValue(key, value);
547
548        if (this.requestProperties.containsKey(key)) {
549            this.requestProperties.get(key).add(value);
550        } else {
551            setRequestProperty(key, value);
552        }
553    }
554
555    /**
556     * Sets an HTTP Request property.
557     * 
558     * @param key request property name
559     * @param value request property value
560     * @see java.net.URLConnection#setRequestProperty(String, String)
561     */
562    public void setRequestProperty(final String key, final String value) {
563        assertNotConnected();
564        assertKeyValue(key, value);
565
566        final List<String> val = new ArrayList<String>();
567        val.add(value);
568        
569        this.requestProperties.put(key, val);
570    }
571    
572    /**
573     * Returns a GSSContextt for the given url with a default lifetime.
574     *  
575     * @param url http address
576     * @return GSSContext for the given url
577     * @throws GSSException
578     * @throws PrivilegedActionException
579     */
580    private GSSContext getGSSContext(final URL url) throws GSSException
581        , PrivilegedActionException {
582
583        if (null == this.credential) {
584            if (null == this.loginContext) {
585                throw new IllegalStateException(
586                        "GSSCredential AND LoginContext NOT initialized");
587                
588            } else {
589                this.credential = SpnegoProvider.getClientCredential(
590                        this.loginContext.getSubject());
591            }
592        }
593        
594        return SpnegoProvider.getGSSContext(this.credential, url);
595    }
596    
597    /**
598     * Returns an error stream that reads from this open connection.
599     * 
600     * @return error stream that reads from this open connection
601     * @throws IOException 
602     * 
603     * @see java.net.HttpURLConnection#getErrorStream()
604     */
605    public InputStream getErrorStream() throws IOException {
606        assertConnected();
607
608        return this.conn.getInputStream();
609    }
610
611    /**
612     * Get header value at specified index.
613     * 
614     * @param index
615     * @return header value at specified index
616     */
617    public String getHeaderField(final int index) {
618        assertConnected();
619        
620        return this.conn.getHeaderField(index);
621    }
622    
623    /**
624     * Get header value by header name.
625     * 
626     * @param name name header
627     * @return header value
628     * @see java.net.HttpURLConnection#getHeaderField(String)
629     */
630    public String getHeaderField(final String name) {
631        assertConnected();
632
633        return this.conn.getHeaderField(name);
634    }
635    
636    /**
637     * Get header field key at specified index.
638     * 
639     * @param index
640     * @return header field key at specified index
641     */
642    public String getHeaderFieldKey(final int index) {
643        assertConnected();
644        
645        return this.conn.getHeaderFieldKey(index);
646    }
647
648    /**
649     * @return true if it should follow redirects
650     * @see java.net.HttpURLConnection#getInstanceFollowRedirects()
651     */
652    public boolean getInstanceFollowRedirects() { // NOPMD
653        return this.instanceFollowRedirects;
654    }
655
656    /**
657     * @param followRedirects 
658     * @see java.net.HttpURLConnection#setInstanceFollowRedirects(boolean)
659     */
660    public void setInstanceFollowRedirects(final boolean followRedirects) {
661        assertNotConnected();
662
663        this.instanceFollowRedirects = followRedirects;
664    }
665
666    /**
667     * Returns an input stream that reads from this open connection.
668     * 
669     * @return input stream that reads from this open connection
670     * @throws IOException 
671     * 
672     * @see java.net.HttpURLConnection#getInputStream()
673     */
674    public InputStream getInputStream() throws IOException {
675        assertConnected();
676
677        return this.conn.getInputStream();
678    }
679    
680    /**
681     * Returns an output stream that writes to this open connection.
682     * 
683     * @return output stream that writes to this connections
684     * @throws IOException
685     * 
686     * @see java.net.HttpURLConnection#getOutputStream()
687     */
688    public OutputStream getOutputStream() throws IOException {
689        assertConnected();
690        
691        return this.conn.getOutputStream();
692    }
693
694    /**
695     * Returns HTTP Status code.
696     * 
697     * @return HTTP Status Code
698     * @throws IOException 
699     * 
700     * @see java.net.HttpURLConnection#getResponseCode()
701     */
702    public int getResponseCode() throws IOException {
703        assertConnected();
704
705        return this.conn.getResponseCode();
706    }
707
708    /**
709     * Returns HTTP Status message.
710     * 
711     * @return HTTP Status Message
712     * @throws IOException 
713     * 
714     * @see java.net.HttpURLConnection#getResponseMessage()
715     */
716    public String getResponseMessage() throws IOException {
717        assertConnected();
718
719        return this.conn.getResponseMessage();
720    }
721    
722    private HttpURLConnection redirect(final URL url, final ByteArrayOutputStream dooutput)
723        throws GSSException, PrivilegedActionException, IOException {
724
725        this.redirectCount++;
726        final String location = this.getHeaderField("location");
727        assert !location.isEmpty();
728        
729        if (!instanceFollowRedirects && '/' != location.charAt(0)) {
730            final URL erl = new URL(location);
731            if (!url.getHost().equalsIgnoreCase(erl.getHost()) 
732                    || url.getPort() != erl.getPort()) {
733                
734                this.redirectCount = SpnegoHttpURLConnection.MAX_REDIRECTS;
735                return this.conn;                
736            }
737        } 
738
739        final List<String> cookies = 
740            this.conn.getHeaderFields().get("Set-Cookie");
741
742        this.conn.disconnect();
743        this.connected = false;
744
745        if (null != cookies) {
746            this.requestProperties.remove("Cookie");
747            for (String cookie : cookies) {
748                this.addRequestProperty("Cookie", cookie);
749            }
750        }
751
752        if ('/' == location.charAt(0)) {
753            final String[] str = url.toString().split("/");
754            final String newLocation = str[0] + "//" + str[2] + location;
755            return this.connect(new URL(newLocation), dooutput);
756        } else {
757            return this.connect(new URL(location), dooutput);
758        }
759    }
760    
761    /**
762     * Request that this GSSCredential be allowed for delegation.
763     * 
764     * @param requestDelegation true to allow/request delegation
765     */
766    public void requestCredDeleg(final boolean requestDelegation) {
767        this.assertNotConnected();
768        
769        this.reqCredDeleg = requestDelegation;
770    }
771    
772    /**
773     * Specify if GSSContext should request Confidentiality.
774     * Default is true.
775     * 
776     * @param confidential pass true for confidentiality
777     */
778    public void setConfidentiality(final boolean confidential) {
779        this.confidentiality = confidential;
780    }
781
782    /**
783     * Specify if GSSContext should request Message Integrity.
784     * Default is true.
785     * 
786     * @param integrity pass true for message integrity
787     */
788    public void setMessageIntegrity(final boolean integrity) {
789        this.messageIntegrity = integrity;
790    }
791
792    /**
793     * Specify if GSSContext should request Mutual Auth.
794     * Default is true.
795     * 
796     * @param mutual pass true for mutual authentication
797     */
798    public void setMutualAuth(final boolean mutual) {
799        this.mutualAuth = mutual;
800    }
801
802    /**
803     * Specify if if GSSContext should request should request Replay Detection.
804     * Default is true.
805     * 
806     * @param replay pass true for replay detection
807     */
808    public void setReplayDetection(final boolean replay) {
809        this.replayDetection = replay;
810    }
811    
812    /**
813     * May override the default GET method.
814     * 
815     * @param method 
816     * 
817     * @see java.net.HttpURLConnection#setRequestMethod(String)
818     */
819    public void setRequestMethod(final String method) {
820        assertNotConnected();
821
822        this.requestMethod = method;
823    }
824    
825    /**
826     * Specify if if GSSContext should request Sequence Detection.
827     * Default is true.
828     * 
829     * @param sequence pass true for sequence detection
830     */
831    public void setSequenceDetection(final boolean sequence) {
832        this.sequenceDetection = sequence;
833    }
834}