1 /*
   2  * Copyright (c) 2005, 2008, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.net.httpserver;
  27 
  28 import java.net.*;
  29 import java.io.*;
  30 import java.nio.*;
  31 import java.security.*;
  32 import java.nio.channels.*;
  33 import java.util.*;
  34 import java.util.concurrent.*;
  35 import java.util.logging.Logger;
  36 import java.util.logging.Level;
  37 import javax.net.ssl.*;
  38 import com.sun.net.httpserver.*;
  39 import com.sun.net.httpserver.spi.*;
  40 import sun.net.httpserver.HttpConnection.State;
  41 
  42 /**
  43  * Provides implementation for both HTTP and HTTPS
  44  */
  45 class ServerImpl implements TimeSource {
  46 
  47     private String protocol;
  48     private boolean https;
  49     private Executor executor;
  50     private HttpsConfigurator httpsConfig;
  51     private SSLContext sslContext;
  52     private ContextList contexts;
  53     private InetSocketAddress address;
  54     private ServerSocketChannel schan;
  55     private Selector selector;
  56     private SelectionKey listenerKey;
  57     private Set<HttpConnection> idleConnections;
  58     private Set<HttpConnection> allConnections;
  59     /* following two are used to keep track of the times
  60      * when a connection/request is first received
  61      * and when we start to send the response
  62      */
  63     private Set<HttpConnection> reqConnections;
  64     private Set<HttpConnection> rspConnections;
  65     private List<Event> events;
  66     private Object lolock = new Object();
  67     private volatile boolean finished = false;
  68     private volatile boolean terminating = false;
  69     private boolean bound = false;
  70     private boolean started = false;
  71     private volatile long time;  /* current time */
  72     private volatile long subticks = 0;
  73     private volatile long ticks; /* number of clock ticks since server started */
  74     private HttpServer wrapper;
  75 
  76     final static int CLOCK_TICK = ServerConfig.getClockTick();
  77     final static long IDLE_INTERVAL = ServerConfig.getIdleInterval();
  78     final static int MAX_IDLE_CONNECTIONS = ServerConfig.getMaxIdleConnections();
  79     final static long TIMER_MILLIS = ServerConfig.getTimerMillis ();
  80     final static long MAX_REQ_TIME=getTimeMillis(ServerConfig.getMaxReqTime());
  81     final static long MAX_RSP_TIME=getTimeMillis(ServerConfig.getMaxRspTime());
  82     final static boolean timer1Enabled = MAX_REQ_TIME != -1 || MAX_RSP_TIME != -1;
  83 
  84     private Timer timer, timer1;
  85     private Logger logger;
  86 
  87     ServerImpl (
  88         HttpServer wrapper, String protocol, InetSocketAddress addr, int backlog
  89     ) throws IOException {
  90 
  91         this.protocol = protocol;
  92         this.wrapper = wrapper;
  93         this.logger = Logger.getLogger ("com.sun.net.httpserver");
  94         ServerConfig.checkLegacyProperties (logger);
  95         https = protocol.equalsIgnoreCase ("https");
  96         this.address = addr;
  97         contexts = new ContextList();
  98         schan = ServerSocketChannel.open();
  99         if (addr != null) {
 100             ServerSocket socket = schan.socket();
 101             socket.bind (addr, backlog);
 102             bound = true;
 103         }
 104         selector = Selector.open ();
 105         schan.configureBlocking (false);
 106         listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);
 107         dispatcher = new Dispatcher();
 108         idleConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());
 109         allConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());
 110         reqConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());
 111         rspConnections = Collections.synchronizedSet (new HashSet<HttpConnection>());
 112         time = System.currentTimeMillis();
 113         timer = new Timer ("server-timer", true);
 114         timer.schedule (new ServerTimerTask(), CLOCK_TICK, CLOCK_TICK);
 115         if (timer1Enabled) {
 116             timer1 = new Timer ("server-timer1", true);
 117             timer1.schedule (new ServerTimerTask1(),TIMER_MILLIS,TIMER_MILLIS);
 118             logger.config ("HttpServer timer1 enabled period in ms:  "+TIMER_MILLIS);
 119             logger.config ("MAX_REQ_TIME:  "+MAX_REQ_TIME);
 120             logger.config ("MAX_RSP_TIME:  "+MAX_RSP_TIME);
 121         }
 122         events = new LinkedList<Event>();
 123         logger.config ("HttpServer created "+protocol+" "+ addr);
 124     }
 125 
 126     public void bind (InetSocketAddress addr, int backlog) throws IOException {
 127         if (bound) {
 128             throw new BindException ("HttpServer already bound");
 129         }
 130         if (addr == null) {
 131             throw new NullPointerException ("null address");
 132         }
 133         ServerSocket socket = schan.socket();
 134         socket.bind (addr, backlog);
 135         bound = true;
 136     }
 137 
 138     public void start () {
 139         if (!bound || started || finished) {
 140             throw new IllegalStateException ("server in wrong state");
 141         }
 142         if (executor == null) {
 143             executor = new DefaultExecutor();
 144         }
 145         Thread t = new Thread (dispatcher);
 146         started = true;
 147         t.start();
 148     }
 149 
 150     public void setExecutor (Executor executor) {
 151         if (started) {
 152             throw new IllegalStateException ("server already started");
 153         }
 154         this.executor = executor;
 155     }
 156 
 157     private static class DefaultExecutor implements Executor {
 158         public void execute (Runnable task) {
 159             task.run();
 160         }
 161     }
 162 
 163     public Executor getExecutor () {
 164         return executor;
 165     }
 166 
 167     public void setHttpsConfigurator (HttpsConfigurator config) {
 168         if (config == null) {
 169             throw new NullPointerException ("null HttpsConfigurator");
 170         }
 171         if (started) {
 172             throw new IllegalStateException ("server already started");
 173         }
 174         this.httpsConfig = config;
 175         sslContext = config.getSSLContext();
 176     }
 177 
 178     public HttpsConfigurator getHttpsConfigurator () {
 179         return httpsConfig;
 180     }
 181 
 182     public void stop (int delay) {
 183         if (delay < 0) {
 184             throw new IllegalArgumentException ("negative delay parameter");
 185         }
 186         terminating = true;
 187         try { schan.close(); } catch (IOException e) {}
 188         selector.wakeup();
 189         long latest = System.currentTimeMillis() + delay * 1000;
 190         while (System.currentTimeMillis() < latest) {
 191             delay();
 192             if (finished) {
 193                 break;
 194             }
 195         }
 196         finished = true;
 197         selector.wakeup();
 198         synchronized (allConnections) {
 199             for (HttpConnection c : allConnections) {
 200                 c.close();
 201             }
 202         }
 203         allConnections.clear();
 204         idleConnections.clear();
 205         timer.cancel();
 206         if (timer1Enabled) {
 207             timer1.cancel();
 208         }
 209     }
 210 
 211     Dispatcher dispatcher;
 212 
 213     public synchronized HttpContextImpl createContext (String path, HttpHandler handler) {
 214         if (handler == null || path == null) {
 215             throw new NullPointerException ("null handler, or path parameter");
 216         }
 217         HttpContextImpl context = new HttpContextImpl (protocol, path, handler, this);
 218         contexts.add (context);
 219         logger.config ("context created: " + path);
 220         return context;
 221     }
 222 
 223     public synchronized HttpContextImpl createContext (String path) {
 224         if (path == null) {
 225             throw new NullPointerException ("null path parameter");
 226         }
 227         HttpContextImpl context = new HttpContextImpl (protocol, path, null, this);
 228         contexts.add (context);
 229         logger.config ("context created: " + path);
 230         return context;
 231     }
 232 
 233     public synchronized void removeContext (String path) throws IllegalArgumentException {
 234         if (path == null) {
 235             throw new NullPointerException ("null path parameter");
 236         }
 237         contexts.remove (protocol, path);
 238         logger.config ("context removed: " + path);
 239     }
 240 
 241     public synchronized void removeContext (HttpContext context) throws IllegalArgumentException {
 242         if (!(context instanceof HttpContextImpl)) {
 243             throw new IllegalArgumentException ("wrong HttpContext type");
 244         }
 245         contexts.remove ((HttpContextImpl)context);
 246         logger.config ("context removed: " + context.getPath());
 247     }
 248 
 249     public InetSocketAddress getAddress() {
 250         return (InetSocketAddress)schan.socket().getLocalSocketAddress();
 251     }
 252 
 253     Selector getSelector () {
 254         return selector;
 255     }
 256 
 257     void addEvent (Event r) {
 258         synchronized (lolock) {
 259             events.add (r);
 260             selector.wakeup();
 261         }
 262     }
 263 
 264     /* main server listener task */
 265 
 266     class Dispatcher implements Runnable {
 267 
 268         private void handleEvent (Event r) {
 269             ExchangeImpl t = r.exchange;
 270             HttpConnection c = t.getConnection();
 271             try {
 272                 if (r instanceof WriteFinishedEvent) {
 273 
 274                     int exchanges = endExchange();
 275                     if (terminating && exchanges == 0) {
 276                         finished = true;
 277                     }
 278                     responseCompleted (c);
 279                     LeftOverInputStream is = t.getOriginalInputStream();
 280                     if (!is.isEOF()) {
 281                         t.close = true;
 282                     }
 283                     if (t.close || idleConnections.size() >= MAX_IDLE_CONNECTIONS) {
 284                         c.close();
 285                         allConnections.remove (c);
 286                     } else {
 287                         if (is.isDataBuffered()) {
 288                             /* don't re-enable the interestops, just handle it */
 289                             requestStarted (c);
 290                             handle (c.getChannel(), c);
 291                         } else {
 292                             connsToRegister.add (c);
 293                         }
 294                     }
 295                 }
 296             } catch (IOException e) {
 297                 logger.log (
 298                     Level.FINER, "Dispatcher (1)", e
 299                 );
 300                 c.close();
 301             }
 302         }
 303 
 304         final LinkedList<HttpConnection> connsToRegister =
 305                 new LinkedList<HttpConnection>();
 306 
 307         void reRegister (HttpConnection c) {
 308             /* re-register with selector */
 309             try {
 310                 SocketChannel chan = c.getChannel();
 311                 chan.configureBlocking (false);
 312                 SelectionKey key = chan.register (selector, SelectionKey.OP_READ);
 313                 key.attach (c);
 314                 c.selectionKey = key;
 315                 c.time = getTime() + IDLE_INTERVAL;
 316                 idleConnections.add (c);
 317             } catch (IOException e) {
 318                 dprint(e);
 319                 logger.log(Level.FINER, "Dispatcher(8)", e);
 320                 c.close();
 321             }
 322         }
 323 
 324         public void run() {
 325             while (!finished) {
 326                 try {
 327                     ListIterator<HttpConnection> li =
 328                         connsToRegister.listIterator();
 329                     for (HttpConnection c : connsToRegister) {
 330                         reRegister(c);
 331                     }
 332                     connsToRegister.clear();
 333 
 334                     List<Event> list = null;
 335                     selector.select(1000);
 336                     synchronized (lolock) {
 337                         if (events.size() > 0) {
 338                             list = events;
 339                             events = new LinkedList<Event>();
 340                         }
 341                     }
 342 
 343                     if (list != null) {
 344                         for (Event r: list) {
 345                             handleEvent (r);
 346                         }
 347                     }
 348 
 349                     /* process the selected list now  */
 350 
 351                     Set<SelectionKey> selected = selector.selectedKeys();
 352                     Iterator<SelectionKey> iter = selected.iterator();
 353                     while (iter.hasNext()) {
 354                         SelectionKey key = iter.next();
 355                         iter.remove ();
 356                         if (key.equals (listenerKey)) {
 357                             if (terminating) {
 358                                 continue;
 359                             }
 360                             SocketChannel chan = schan.accept();
 361                             if (chan == null) {
 362                                 continue; /* cancel something ? */
 363                             }
 364                             chan.configureBlocking (false);
 365                             SelectionKey newkey = chan.register (selector, SelectionKey.OP_READ);
 366                             HttpConnection c = new HttpConnection ();
 367                             c.selectionKey = newkey;
 368                             c.setChannel (chan);
 369                             newkey.attach (c);
 370                             requestStarted (c);
 371                             allConnections.add (c);
 372                         } else {
 373                             try {
 374                                 if (key.isReadable()) {
 375                                     boolean closed;
 376                                     SocketChannel chan = (SocketChannel)key.channel();
 377                                     HttpConnection conn = (HttpConnection)key.attachment();
 378 
 379                                     key.cancel();
 380                                     chan.configureBlocking (true);
 381                                     if (idleConnections.remove(conn)) {
 382                                         // was an idle connection so add it
 383                                         // to reqConnections set.
 384                                         requestStarted (conn);
 385                                     }
 386                                     handle (chan, conn);
 387                                 } else {
 388                                     assert false;
 389                                 }
 390                             } catch (CancelledKeyException e) {
 391                                 handleException(key, null);
 392                             } catch (IOException e) {
 393                                 handleException(key, e);
 394                             }
 395                         }
 396                     }
 397                     // call the selector just to process the cancelled keys
 398                     selector.selectNow();
 399                 } catch (IOException e) {
 400                     logger.log (Level.FINER, "Dispatcher (4)", e);
 401                 } catch (Exception e) {
 402                     e.printStackTrace();
 403                     logger.log (Level.FINER, "Dispatcher (7)", e);
 404                 }
 405             }
 406         }
 407 
 408         private void handleException (SelectionKey key, Exception e) {
 409             HttpConnection conn = (HttpConnection)key.attachment();
 410             if (e != null) {
 411                 logger.log (Level.FINER, "Dispatcher (2)", e);
 412             }
 413             closeConnection(conn);
 414         }
 415 
 416         public void handle (SocketChannel chan, HttpConnection conn)
 417         throws IOException
 418         {
 419             try {
 420                 Exchange t = new Exchange (chan, protocol, conn);
 421                 executor.execute (t);
 422             } catch (HttpError e1) {
 423                 logger.log (Level.FINER, "Dispatcher (4)", e1);
 424                 closeConnection(conn);
 425             } catch (IOException e) {
 426                 logger.log (Level.FINER, "Dispatcher (5)", e);
 427                 closeConnection(conn);
 428             }
 429         }
 430     }
 431 
 432     static boolean debug = ServerConfig.debugEnabled ();
 433 
 434     static synchronized void dprint (String s) {
 435         if (debug) {
 436             System.out.println (s);
 437         }
 438     }
 439 
 440     static synchronized void dprint (Exception e) {
 441         if (debug) {
 442             System.out.println (e);
 443             e.printStackTrace();
 444         }
 445     }
 446 
 447     Logger getLogger () {
 448         return logger;
 449     }
 450 
 451     private void closeConnection(HttpConnection conn) {
 452         conn.close();
 453         allConnections.remove(conn);
 454         switch (conn.getState()) {
 455         case REQUEST:
 456             reqConnections.remove(conn);
 457             break;
 458         case RESPONSE:
 459             rspConnections.remove(conn);
 460             break;
 461         case IDLE:
 462             idleConnections.remove(conn);
 463             break;
 464         }
 465         assert !reqConnections.remove(conn);
 466         assert !rspConnections.remove(conn);
 467         assert !idleConnections.remove(conn);
 468     }
 469 
 470         /* per exchange task */
 471 
 472     class Exchange implements Runnable {
 473         SocketChannel chan;
 474         HttpConnection connection;
 475         HttpContextImpl context;
 476         InputStream rawin;
 477         OutputStream rawout;
 478         String protocol;
 479         ExchangeImpl tx;
 480         HttpContextImpl ctx;
 481         boolean rejected = false;
 482 
 483         Exchange (SocketChannel chan, String protocol, HttpConnection conn) throws IOException {
 484             this.chan = chan;
 485             this.connection = conn;
 486             this.protocol = protocol;
 487         }
 488 
 489         public void run () {
 490             /* context will be null for new connections */
 491             context = connection.getHttpContext();
 492             boolean newconnection;
 493             SSLEngine engine = null;
 494             String requestLine = null;
 495             SSLStreams sslStreams = null;
 496             try {
 497                 if (context != null ) {
 498                     this.rawin = connection.getInputStream();
 499                     this.rawout = connection.getRawOutputStream();
 500                     newconnection = false;
 501                 } else {
 502                     /* figure out what kind of connection this is */
 503                     newconnection = true;
 504                     if (https) {
 505                         if (sslContext == null) {
 506                             logger.warning ("SSL connection received. No https contxt created");
 507                             throw new HttpError ("No SSL context established");
 508                         }
 509                         sslStreams = new SSLStreams (ServerImpl.this, sslContext, chan);
 510                         rawin = sslStreams.getInputStream();
 511                         rawout = sslStreams.getOutputStream();
 512                         engine = sslStreams.getSSLEngine();
 513                         connection.sslStreams = sslStreams;
 514                     } else {
 515                         rawin = new BufferedInputStream(
 516                             new Request.ReadStream (
 517                                 ServerImpl.this, chan
 518                         ));
 519                         rawout = new Request.WriteStream (
 520                             ServerImpl.this, chan
 521                         );
 522                     }
 523                     connection.raw = rawin;
 524                     connection.rawout = rawout;
 525                 }
 526                 Request req = new Request (rawin, rawout);
 527                 requestLine = req.requestLine();
 528                 if (requestLine == null) {
 529                     /* connection closed */
 530                     closeConnection(connection);
 531                     return;
 532                 }
 533                 int space = requestLine.indexOf (' ');
 534                 if (space == -1) {
 535                     reject (Code.HTTP_BAD_REQUEST,
 536                             requestLine, "Bad request line");
 537                     return;
 538                 }
 539                 String method = requestLine.substring (0, space);
 540                 int start = space+1;
 541                 space = requestLine.indexOf(' ', start);
 542                 if (space == -1) {
 543                     reject (Code.HTTP_BAD_REQUEST,
 544                             requestLine, "Bad request line");
 545                     return;
 546                 }
 547                 String uriStr = requestLine.substring (start, space);
 548                 URI uri = new URI (uriStr);
 549                 start = space+1;
 550                 String version = requestLine.substring (start);
 551                 Headers headers = req.headers();
 552                 String s = headers.getFirst ("Transfer-encoding");
 553                 long clen = 0L;
 554                 if (s !=null && s.equalsIgnoreCase ("chunked")) {
 555                     clen = -1L;
 556                 } else {
 557                     s = headers.getFirst ("Content-Length");
 558                     if (s != null) {
 559                         clen = Long.parseLong(s);
 560                     }
 561                     if (clen == 0) {
 562                         requestCompleted (connection);
 563                     }
 564                 }
 565                 ctx = contexts.findContext (protocol, uri.getPath());
 566                 if (ctx == null) {
 567                     reject (Code.HTTP_NOT_FOUND,
 568                             requestLine, "No context found for request");
 569                     return;
 570                 }
 571                 connection.setContext (ctx);
 572                 if (ctx.getHandler() == null) {
 573                     reject (Code.HTTP_INTERNAL_ERROR,
 574                             requestLine, "No handler for context");
 575                     return;
 576                 }
 577                 tx = new ExchangeImpl (
 578                     method, uri, req, clen, connection
 579                 );
 580                 String chdr = headers.getFirst("Connection");
 581                 Headers rheaders = tx.getResponseHeaders();
 582 
 583                 if (chdr != null && chdr.equalsIgnoreCase ("close")) {
 584                     tx.close = true;
 585                 }
 586                 if (version.equalsIgnoreCase ("http/1.0")) {
 587                     tx.http10 = true;
 588                     if (chdr == null) {
 589                         tx.close = true;
 590                         rheaders.set ("Connection", "close");
 591                     } else if (chdr.equalsIgnoreCase ("keep-alive")) {
 592                         rheaders.set ("Connection", "keep-alive");
 593                         int idle=(int)ServerConfig.getIdleInterval()/1000;
 594                         int max=(int)ServerConfig.getMaxIdleConnections();
 595                         String val = "timeout="+idle+", max="+max;
 596                         rheaders.set ("Keep-Alive", val);
 597                     }
 598                 }
 599 
 600                 if (newconnection) {
 601                     connection.setParameters (
 602                         rawin, rawout, chan, engine, sslStreams,
 603                         sslContext, protocol, ctx, rawin
 604                     );
 605                 }
 606                 /* check if client sent an Expect 100 Continue.
 607                  * In that case, need to send an interim response.
 608                  * In future API may be modified to allow app to
 609                  * be involved in this process.
 610                  */
 611                 String exp = headers.getFirst("Expect");
 612                 if (exp != null && exp.equalsIgnoreCase ("100-continue")) {
 613                     logReply (100, requestLine, null);
 614                     sendReply (
 615                         Code.HTTP_CONTINUE, false, null
 616                     );
 617                 }
 618                 /* uf is the list of filters seen/set by the user.
 619                  * sf is the list of filters established internally
 620                  * and which are not visible to the user. uc and sc
 621                  * are the corresponding Filter.Chains.
 622                  * They are linked together by a LinkHandler
 623                  * so that they can both be invoked in one call.
 624                  */
 625                 List<Filter> sf = ctx.getSystemFilters();
 626                 List<Filter> uf = ctx.getFilters();
 627 
 628                 Filter.Chain sc = new Filter.Chain(sf, ctx.getHandler());
 629                 Filter.Chain uc = new Filter.Chain(uf, new LinkHandler (sc));
 630 
 631                 /* set up the two stream references */
 632                 tx.getRequestBody();
 633                 tx.getResponseBody();
 634                 if (https) {
 635                     uc.doFilter (new HttpsExchangeImpl (tx));
 636                 } else {
 637                     uc.doFilter (new HttpExchangeImpl (tx));
 638                 }
 639 
 640             } catch (IOException e1) {
 641                 logger.log (Level.FINER, "ServerImpl.Exchange (1)", e1);
 642                 closeConnection(connection);
 643             } catch (NumberFormatException e3) {
 644                 reject (Code.HTTP_BAD_REQUEST,
 645                         requestLine, "NumberFormatException thrown");
 646             } catch (URISyntaxException e) {
 647                 reject (Code.HTTP_BAD_REQUEST,
 648                         requestLine, "URISyntaxException thrown");
 649             } catch (Exception e4) {
 650                 logger.log (Level.FINER, "ServerImpl.Exchange (2)", e4);
 651                 closeConnection(connection);
 652             }
 653         }
 654 
 655         /* used to link to 2 or more Filter.Chains together */
 656 
 657         class LinkHandler implements HttpHandler {
 658             Filter.Chain nextChain;
 659 
 660             LinkHandler (Filter.Chain nextChain) {
 661                 this.nextChain = nextChain;
 662             }
 663 
 664             public void handle (HttpExchange exchange) throws IOException {
 665                 nextChain.doFilter (exchange);
 666             }
 667         }
 668 
 669         void reject (int code, String requestStr, String message) {
 670             rejected = true;
 671             logReply (code, requestStr, message);
 672             sendReply (
 673                 code, false, "<h1>"+code+Code.msg(code)+"</h1>"+message
 674             );
 675             closeConnection(connection);
 676         }
 677 
 678         void sendReply (
 679             int code, boolean closeNow, String text)
 680         {
 681             try {
 682                 StringBuilder builder = new StringBuilder (512);
 683                 builder.append ("HTTP/1.1 ")
 684                     .append (code).append (Code.msg(code)).append ("\r\n");
 685 
 686                 if (text != null && text.length() != 0) {
 687                     builder.append ("Content-Length: ")
 688                         .append (text.length()).append ("\r\n")
 689                         .append ("Content-Type: text/html\r\n");
 690                 } else {
 691                     builder.append ("Content-Length: 0\r\n");
 692                     text = "";
 693                 }
 694                 if (closeNow) {
 695                     builder.append ("Connection: close\r\n");
 696                 }
 697                 builder.append ("\r\n").append (text);
 698                 String s = builder.toString();
 699                 byte[] b = s.getBytes("ISO8859_1");
 700                 rawout.write (b);
 701                 rawout.flush();
 702                 if (closeNow) {
 703                     closeConnection(connection);
 704                 }
 705             } catch (IOException e) {
 706                 logger.log (Level.FINER, "ServerImpl.sendReply", e);
 707                 closeConnection(connection);
 708             }
 709         }
 710 
 711     }
 712 
 713     void logReply (int code, String requestStr, String text) {
 714         if (!logger.isLoggable(Level.FINE)) {
 715             return;
 716         }
 717         if (text == null) {
 718             text = "";
 719         }
 720         String r;
 721         if (requestStr.length() > 80) {
 722            r = requestStr.substring (0, 80) + "<TRUNCATED>";
 723         } else {
 724            r = requestStr;
 725         }
 726         String message = r + " [" + code + " " +
 727                     Code.msg(code) + "] ("+text+")";
 728         logger.fine (message);
 729     }
 730 
 731     long getTicks() {
 732         return ticks;
 733     }
 734 
 735     public long getTime() {
 736         return time;
 737     }
 738 
 739     void delay () {
 740         Thread.yield();
 741         try {
 742             Thread.sleep (200);
 743         } catch (InterruptedException e) {}
 744     }
 745 
 746     private int exchangeCount = 0;
 747 
 748     synchronized void startExchange () {
 749         exchangeCount ++;
 750     }
 751 
 752     synchronized int endExchange () {
 753         exchangeCount --;
 754         assert exchangeCount >= 0;
 755         return exchangeCount;
 756     }
 757 
 758     HttpServer getWrapper () {
 759         return wrapper;
 760     }
 761 
 762     void requestStarted (HttpConnection c) {
 763         c.creationTime = getTime();
 764         c.setState (State.REQUEST);
 765         reqConnections.add (c);
 766     }
 767 
 768     // called after a request has been completely read
 769     // by the server. This stops the timer which would
 770     // close the connection if the request doesn't arrive
 771     // quickly enough. It then starts the timer
 772     // that ensures the client reads the response in a timely
 773     // fashion.
 774 
 775     void requestCompleted (HttpConnection c) {
 776         assert c.getState() == State.REQUEST;
 777         reqConnections.remove (c);
 778         c.rspStartedTime = getTime();
 779         rspConnections.add (c);
 780         c.setState (State.RESPONSE);
 781     }
 782 
 783     // called after response has been sent
 784     void responseCompleted (HttpConnection c) {
 785         assert c.getState() == State.RESPONSE;
 786         rspConnections.remove (c);
 787         c.setState (State.IDLE);
 788     }
 789 
 790     /**
 791      * TimerTask run every CLOCK_TICK ms
 792      */
 793     class ServerTimerTask extends TimerTask {
 794         public void run () {
 795             LinkedList<HttpConnection> toClose = new LinkedList<HttpConnection>();
 796             time = System.currentTimeMillis();
 797             ticks ++;
 798             synchronized (idleConnections) {
 799                 for (HttpConnection c : idleConnections) {
 800                     if (c.time <= time) {
 801                         toClose.add (c);
 802                     }
 803                 }
 804                 for (HttpConnection c : toClose) {
 805                     idleConnections.remove (c);
 806                     allConnections.remove (c);
 807                     c.close();
 808                 }
 809             }
 810         }
 811     }
 812 
 813     class ServerTimerTask1 extends TimerTask {
 814 
 815         // runs every TIMER_MILLIS
 816         public void run () {
 817             LinkedList<HttpConnection> toClose = new LinkedList<HttpConnection>();
 818             time = System.currentTimeMillis();
 819             synchronized (reqConnections) {
 820                 if (MAX_REQ_TIME != -1) {
 821                     for (HttpConnection c : reqConnections) {
 822                         if (c.creationTime + TIMER_MILLIS + MAX_REQ_TIME <= time) {
 823                             toClose.add (c);
 824                         }
 825                     }
 826                     for (HttpConnection c : toClose) {
 827                         logger.log (Level.FINE, "closing: no request: " + c);
 828                         reqConnections.remove (c);
 829                         allConnections.remove (c);
 830                         c.close();
 831                     }
 832                 }
 833             }
 834             toClose = new LinkedList<HttpConnection>();
 835             synchronized (rspConnections) {
 836                 if (MAX_RSP_TIME != -1) {
 837                     for (HttpConnection c : rspConnections) {
 838                         if (c.rspStartedTime + TIMER_MILLIS +MAX_RSP_TIME <= time) {
 839                             toClose.add (c);
 840                         }
 841                     }
 842                     for (HttpConnection c : toClose) {
 843                         logger.log (Level.FINE, "closing: no response: " + c);
 844                         rspConnections.remove (c);
 845                         allConnections.remove (c);
 846                         c.close();
 847                     }
 848                 }
 849             }
 850         }
 851     }
 852 
 853     void logStackTrace (String s) {
 854         logger.finest (s);
 855         StringBuilder b = new StringBuilder ();
 856         StackTraceElement[] e = Thread.currentThread().getStackTrace();
 857         for (int i=0; i<e.length; i++) {
 858             b.append (e[i].toString()).append("\n");
 859         }
 860         logger.finest (b.toString());
 861     }
 862 
 863     static long getTimeMillis(long secs) {
 864         if (secs == -1) {
 865             return -1;
 866         } else {
 867             return secs * 1000;
 868         }
 869     }
 870 }