1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.core;
18
19import android.util.Log;
20
21import java.io.*;
22import java.lang.Thread;
23import java.net.*;
24import java.util.*;
25
26/**
27 * TestWebServer is a simulated controllable test server that
28 * can respond to requests from HTTP clients.
29 *
30 * The server can be controlled to change how it reacts to any
31 * requests, and can be told to simulate various events (such as
32 * network failure) that would happen in a real environment.
33 */
34class TestWebServer implements HttpConstants {
35
36    /* static class data/methods */
37
38    /* The ANDROID_LOG_TAG */
39    private final static String LOGTAG = "httpsv";
40
41    /* Where worker threads stand idle */
42    Vector threads = new Vector();
43
44    /* List of all active worker threads */
45    Vector activeThreads = new Vector();
46
47    /* timeout on client connections */
48    int timeout = 0;
49
50    /* max # worker threads */
51    int workers = 5;
52
53    /* Default port for this server to listen on */
54    final static int DEFAULT_PORT = 8080;
55
56    /* Default socket timeout value */
57    final static int DEFAULT_TIMEOUT = 5000;
58
59    /* Version string (configurable) */
60    protected String HTTP_VERSION_STRING = "HTTP/1.1";
61
62    /* Indicator for whether this server is configured as a HTTP/1.1
63     * or HTTP/1.0 server
64     */
65    private boolean http11 = true;
66
67    /* The thread handling new requests from clients */
68    private AcceptThread acceptT;
69
70    /* timeout on client connections */
71    int mTimeout;
72
73    /* Server port */
74    int mPort;
75
76    /* Switch on/off logging */
77    boolean mLog = false;
78
79    /* If set, this will keep connections alive after a request has been
80     * processed.
81     */
82    boolean keepAlive = true;
83
84    /* If set, this will cause response data to be sent in 'chunked' format */
85    boolean chunked = false;
86
87    /* If set, this will indicate a new redirection host */
88    String redirectHost = null;
89
90    /* If set, this indicates the reason for redirection */
91    int redirectCode = -1;
92
93    /* Set the number of connections the server will accept before shutdown */
94    int acceptLimit = 100;
95
96    /* Count of number of accepted connections */
97    int acceptedConnections = 0;
98
99    public TestWebServer() {
100    }
101
102    /**
103     * Initialize a new server with default port and timeout.
104     * @param log Set true if you want trace output
105     */
106    public void initServer(boolean log) throws Exception {
107        initServer(DEFAULT_PORT, DEFAULT_TIMEOUT, log);
108    }
109
110    /**
111     * Initialize a new server with default timeout.
112     * @param port Sets the server to listen on this port
113     * @param log Set true if you want trace output
114     */
115    public void initServer(int port, boolean log) throws Exception {
116        initServer(port, DEFAULT_TIMEOUT, log);
117    }
118
119    /**
120     * Initialize a new server with default port and timeout.
121     * @param port Sets the server to listen on this port
122     * @param timeout Indicates the period of time to wait until a socket is
123     *                closed
124     * @param log Set true if you want trace output
125     */
126    public void initServer(int port, int timeout, boolean log) throws Exception {
127        mPort = port;
128        mTimeout = timeout;
129        mLog = log;
130        keepAlive = true;
131
132        if (acceptT == null) {
133            acceptT = new AcceptThread();
134            acceptT.init();
135            acceptT.start();
136        }
137    }
138
139    /**
140     * Print to the log file (if logging enabled)
141     * @param s String to send to the log
142     */
143    protected void log(String s) {
144        if (mLog) {
145            Log.d(LOGTAG, s);
146        }
147    }
148
149    /**
150     * Set the server to be an HTTP/1.0 or HTTP/1.1 server.
151     * This should be called prior to any requests being sent
152     * to the server.
153     * @param set True for the server to be HTTP/1.1, false for HTTP/1.0
154     */
155    public void setHttpVersion11(boolean set) {
156        http11 = set;
157        if (set) {
158            HTTP_VERSION_STRING = "HTTP/1.1";
159        } else {
160            HTTP_VERSION_STRING = "HTTP/1.0";
161        }
162    }
163
164    /**
165     * Call this to determine whether server connection should remain open
166     * @param value Set true to keep connections open after a request
167     *              completes
168     */
169    public void setKeepAlive(boolean value) {
170        keepAlive = value;
171    }
172
173    /**
174     * Call this to indicate whether chunked data should be used
175     * @param value Set true to make server respond with chunk encoded
176     *              content data.
177     */
178    public void setChunked(boolean value) {
179        chunked = value;
180    }
181
182    /**
183     * Call this to specify the maximum number of sockets to accept
184     * @param limit The number of sockets to accept
185     */
186    public void setAcceptLimit(int limit) {
187        acceptLimit = limit;
188    }
189
190    /**
191     * Call this to indicate redirection port requirement.
192     * When this value is set, the server will respond to a request with
193     * a redirect code with the Location response header set to the value
194     * specified.
195     * @param redirect The location to be redirected to
196     * @param redirectCode The code to send when redirecting
197     */
198    public void setRedirect(String redirect, int code) {
199        redirectHost = redirect;
200        redirectCode = code;
201        log("Server will redirect output to "+redirect+" code "+code);
202    }
203
204    /**
205     * Cause the thread accepting connections on the server socket to close
206     */
207    public void close() {
208        /* Stop the Accept thread */
209        if (acceptT != null) {
210            log("Closing AcceptThread"+acceptT);
211            acceptT.close();
212            acceptT = null;
213        }
214    }
215    /**
216     * The AcceptThread is responsible for initiating worker threads
217     * to handle incoming requests from clients.
218     */
219    class AcceptThread extends Thread {
220
221        ServerSocket ss = null;
222        boolean running = false;
223
224        public void init() {
225            // Networking code doesn't support ServerSocket(port) yet
226            InetSocketAddress ia = new InetSocketAddress(mPort);
227            while (true) {
228                try {
229                    ss = new ServerSocket();
230                    // Socket timeout functionality is not available yet
231                    //ss.setSoTimeout(5000);
232                    ss.setReuseAddress(true);
233                    ss.bind(ia);
234                    break;
235                } catch (IOException e) {
236                    log("IOException in AcceptThread.init()");
237                    e.printStackTrace();
238                    // wait and retry
239                    try {
240                        Thread.sleep(1000);
241                    } catch (InterruptedException e1) {
242                        // TODO Auto-generated catch block
243                        e1.printStackTrace();
244                    }
245                }
246            }
247        }
248
249        /**
250         * Main thread responding to new connections
251         */
252        public synchronized void run() {
253            running = true;
254            try {
255                while (running) {
256                    // Log.d(LOGTAG, "TestWebServer run() calling accept()");
257                    Socket s = ss.accept();
258                    acceptedConnections++;
259                    if (acceptedConnections >= acceptLimit) {
260                        running = false;
261                    }
262
263                    Worker w = null;
264                    synchronized (threads) {
265                        if (threads.isEmpty()) {
266                            Worker ws = new Worker();
267                            ws.setSocket(s);
268                            activeThreads.addElement(ws);
269                            (new Thread(ws, "additional worker")).start();
270                        } else {
271                            w = (Worker) threads.elementAt(0);
272                            threads.removeElementAt(0);
273                            w.setSocket(s);
274                        }
275                    }
276                }
277            } catch (SocketException e) {
278                log("SocketException in AcceptThread: probably closed during accept");
279                running = false;
280            } catch (IOException e) {
281                log("IOException in AcceptThread");
282                e.printStackTrace();
283                running = false;
284            }
285            log("AcceptThread terminated" + this);
286        }
287
288        // Close this socket
289        public void close() {
290            try {
291                running = false;
292                /* Stop server socket from processing further. Currently
293                   this does not cause the SocketException from ss.accept
294                   therefore the acceptLimit functionality has been added
295                   to circumvent this limitation */
296                ss.close();
297
298                // Stop worker threads from continuing
299                for (Enumeration e = activeThreads.elements(); e.hasMoreElements();) {
300                    Worker w = (Worker)e.nextElement();
301                    w.close();
302                }
303                activeThreads.clear();
304
305            } catch (IOException e) {
306                /* We are shutting down the server, so we expect
307                 * things to die. Don't propagate.
308                 */
309                log("IOException caught by server socket close");
310            }
311        }
312    }
313
314    // Size of buffer for reading from the connection
315    final static int BUF_SIZE = 2048;
316
317    /* End of line byte sequence */
318    static final byte[] EOL = {(byte)'\r', (byte)'\n' };
319
320    /**
321     * The worker thread handles all interactions with a current open
322     * connection. If pipelining is turned on, this will allow this
323     * thread to continuously operate on numerous requests before the
324     * connection is closed.
325     */
326    class Worker implements HttpConstants, Runnable {
327
328        /* buffer to use to hold request data */
329        byte[] buf;
330
331        /* Socket to client we're handling */
332        private Socket s;
333
334        /* Reference to current request method ID */
335        private int requestMethod;
336
337        /* Reference to current requests test file/data */
338        private String testID;
339
340        /* Reference to test number from testID */
341        private int testNum;
342
343        /* Reference to whether new request has been initiated yet */
344        private boolean readStarted;
345
346        /* Indicates whether current request has any data content */
347        private boolean hasContent = false;
348
349        boolean running = false;
350
351        /* Request headers are stored here */
352        private Hashtable<String, String> headers = new Hashtable<String, String>();
353
354        /* Create a new worker thread */
355        Worker() {
356            buf = new byte[BUF_SIZE];
357            s = null;
358        }
359
360        /**
361         * Called by the AcceptThread to unblock this Worker to process
362         * a request.
363         * @param s The socket on which the connection has been made
364         */
365        synchronized void setSocket(Socket s) {
366            this.s = s;
367            notify();
368        }
369
370        /**
371         * Called by the accept thread when it's closing. Potentially unblocks
372         * the worker thread to terminate properly
373         */
374        synchronized void close() {
375            running = false;
376            notify();
377        }
378
379        /**
380         * Main worker thread. This will wait until a request has
381         * been identified by the accept thread upon which it will
382         * service the thread.
383         */
384        public synchronized void run() {
385            running = true;
386            while(running) {
387                if (s == null) {
388                    /* nothing to do */
389                    try {
390                        log(this+" Moving to wait state");
391                        wait();
392                    } catch (InterruptedException e) {
393                        /* should not happen */
394                        continue;
395                    }
396                    if (!running) break;
397                }
398                try {
399                    handleClient();
400                } catch (Exception e) {
401                    e.printStackTrace();
402                }
403                /* go back in wait queue if there's fewer
404                 * than numHandler connections.
405                 */
406                s = null;
407                Vector pool = threads;
408                synchronized (pool) {
409                    if (pool.size() >= workers) {
410                        /* too many threads, exit this one */
411                        activeThreads.remove(this);
412                        return;
413                    } else {
414                        pool.addElement(this);
415                    }
416                }
417            }
418            log(this+" terminated");
419        }
420
421        /**
422         * Zero out the buffer from last time
423         */
424        private void clearBuffer() {
425            for (int i = 0; i < BUF_SIZE; i++) {
426                buf[i] = 0;
427            }
428        }
429
430        /**
431         * Utility method to read a line of data from the input stream
432         * @param is Inputstream to read
433         * @return number of bytes read
434         */
435        private int readOneLine(InputStream is) {
436
437            int read = 0;
438
439            clearBuffer();
440            try {
441                log("Reading one line: started ="+readStarted+" avail="+is.available());
442                while ((!readStarted) || (is.available() > 0)) {
443                    int data = is.read();
444                    // We shouldn't get EOF but we need tdo check
445                    if (data == -1) {
446                        log("EOF returned");
447                        return -1;
448                    }
449
450                    buf[read] = (byte)data;
451
452                    System.out.print((char)data);
453
454                    readStarted = true;
455                    if (buf[read++]==(byte)'\n') {
456                        System.out.println();
457                        return read;
458                    }
459                }
460            } catch (IOException e) {
461                log("IOException from readOneLine");
462                e.printStackTrace();
463            }
464            return read;
465        }
466
467        /**
468         * Read a chunk of data
469         * @param is Stream from which to read data
470         * @param length Amount of data to read
471         * @return number of bytes read
472         */
473        private int readData(InputStream is, int length) {
474            int read = 0;
475            int count;
476            // At the moment we're only expecting small data amounts
477            byte[] buf = new byte[length];
478
479            try {
480                while (is.available() > 0) {
481                    count = is.read(buf, read, length-read);
482                    read += count;
483                }
484            } catch (IOException e) {
485                log("IOException from readData");
486                e.printStackTrace();
487            }
488            return read;
489        }
490
491        /**
492         * Read the status line from the input stream extracting method
493         * information.
494         * @param is Inputstream to read
495         * @return number of bytes read
496         */
497        private int parseStatusLine(InputStream is) {
498            int index;
499            int nread = 0;
500
501            log("Parse status line");
502            // Check for status line first
503            nread = readOneLine(is);
504            // Bomb out if stream closes prematurely
505            if (nread == -1) {
506                requestMethod = UNKNOWN_METHOD;
507                return -1;
508            }
509
510            if (buf[0] == (byte)'G' &&
511                buf[1] == (byte)'E' &&
512                buf[2] == (byte)'T' &&
513                buf[3] == (byte)' ') {
514                requestMethod = GET_METHOD;
515                log("GET request");
516                index = 4;
517            } else if (buf[0] == (byte)'H' &&
518                       buf[1] == (byte)'E' &&
519                       buf[2] == (byte)'A' &&
520                       buf[3] == (byte)'D' &&
521                       buf[4] == (byte)' ') {
522                requestMethod = HEAD_METHOD;
523                log("HEAD request");
524                index = 5;
525            } else if (buf[0] == (byte)'P' &&
526                       buf[1] == (byte)'O' &&
527                       buf[2] == (byte)'S' &&
528                       buf[3] == (byte)'T' &&
529                       buf[4] == (byte)' ') {
530                requestMethod = POST_METHOD;
531                log("POST request");
532                index = 5;
533            } else {
534                // Unhandled request
535                requestMethod = UNKNOWN_METHOD;
536                return -1;
537            }
538
539            // A valid method we understand
540            if (requestMethod > UNKNOWN_METHOD) {
541                // Read file name
542                int i = index;
543                while (buf[i] != (byte)' ') {
544                    // There should be HTTP/1.x at the end
545                    if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
546                        requestMethod = UNKNOWN_METHOD;
547                        return -1;
548                    }
549                    i++;
550                }
551
552                testID = new String(buf, 0, index, i-index);
553                if (testID.startsWith("/")) {
554                    testID = testID.substring(1);
555                }
556
557                return nread;
558            }
559            return -1;
560        }
561
562        /**
563         * Read a header from the input stream
564         * @param is Inputstream to read
565         * @return number of bytes read
566         */
567        private int parseHeader(InputStream is) {
568            int index = 0;
569            int nread = 0;
570            log("Parse a header");
571            // Check for status line first
572            nread = readOneLine(is);
573            // Bomb out if stream closes prematurely
574            if (nread == -1) {
575                requestMethod = UNKNOWN_METHOD;
576                return -1;
577            }
578            // Read header entry 'Header: data'
579            int i = index;
580            while (buf[i] != (byte)':') {
581                // There should be an entry after the header
582
583                if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
584                    return UNKNOWN_METHOD;
585                }
586                i++;
587            }
588
589            String headerName = new String(buf, 0, i);
590            i++; // Over ':'
591            while (buf[i] == ' ') {
592                i++;
593            }
594            String headerValue = new String(buf, i, nread-1);
595
596            headers.put(headerName, headerValue);
597            return nread;
598        }
599
600        /**
601         * Read all headers from the input stream
602         * @param is Inputstream to read
603         * @return number of bytes read
604         */
605        private int readHeaders(InputStream is) {
606            int nread = 0;
607            log("Read headers");
608            // Headers should be terminated by empty CRLF line
609            while (true) {
610                int headerLen = 0;
611                headerLen = parseHeader(is);
612                if (headerLen == -1)
613                    return -1;
614                nread += headerLen;
615                if (headerLen <= 2) {
616                    return nread;
617                }
618            }
619        }
620
621        /**
622         * Read content data from the input stream
623         * @param is Inputstream to read
624         * @return number of bytes read
625         */
626        private int readContent(InputStream is) {
627            int nread = 0;
628            log("Read content");
629            String lengthString = headers.get(requestHeaders[REQ_CONTENT_LENGTH]);
630            int length = new Integer(lengthString).intValue();
631
632            // Read content
633            length = readData(is, length);
634            return length;
635        }
636
637        /**
638         * The main loop, reading requests.
639         */
640        void handleClient() throws IOException {
641            InputStream is = new BufferedInputStream(s.getInputStream());
642            PrintStream ps = new PrintStream(s.getOutputStream());
643            int nread = 0;
644
645            /* we will only block in read for this many milliseconds
646             * before we fail with java.io.InterruptedIOException,
647             * at which point we will abandon the connection.
648             */
649            s.setSoTimeout(mTimeout);
650            s.setTcpNoDelay(true);
651
652            do {
653                nread = parseStatusLine(is);
654                if (requestMethod != UNKNOWN_METHOD) {
655
656                    // If status line found, read any headers
657                    nread = readHeaders(is);
658
659                    // Then read content (if any)
660                    // TODO handle chunked encoding from the client
661                    if (headers.get(requestHeaders[REQ_CONTENT_LENGTH]) != null) {
662                        nread = readContent(is);
663                    }
664                } else {
665                    if (nread > 0) {
666                        /* we don't support this method */
667                        ps.print(HTTP_VERSION_STRING + " " + HTTP_BAD_METHOD +
668                                 " unsupported method type: ");
669                        ps.write(buf, 0, 5);
670                        ps.write(EOL);
671                        ps.flush();
672                    } else {
673                    }
674                    if (!keepAlive || nread <= 0) {
675                        headers.clear();
676                        readStarted = false;
677
678                        log("SOCKET CLOSED");
679                        s.close();
680                        return;
681                    }
682                }
683
684                // Reset test number prior to outputing data
685                testNum = -1;
686
687                // Write out the data
688                printStatus(ps);
689                printHeaders(ps);
690
691                // Write line between headers and body
692                psWriteEOL(ps);
693
694                // Write the body
695                if (redirectCode == -1) {
696                    switch (requestMethod) {
697                        case GET_METHOD:
698                            if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) {
699                                send404(ps);
700                            } else {
701                                sendFile(ps);
702                            }
703                            break;
704                        case HEAD_METHOD:
705                            // Nothing to do
706                            break;
707                        case POST_METHOD:
708                            // Post method write body data
709                            if ((testNum > 0) || (testNum < TestWebData.tests.length - 1)) {
710                                sendFile(ps);
711                            }
712
713                            break;
714                        default:
715                            break;
716                    }
717                } else { // Redirecting
718                    switch (redirectCode) {
719                        case 301:
720                            // Seems 301 needs a body by neon (although spec
721                            // says SHOULD).
722                            psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_301]);
723                            break;
724                        case 302:
725                            //
726                            psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_302]);
727                            break;
728                        case 303:
729                            psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_303]);
730                            break;
731                        case 307:
732                            psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_307]);
733                            break;
734                        default:
735                            break;
736                    }
737                }
738
739                ps.flush();
740
741                // Reset for next request
742                readStarted = false;
743                headers.clear();
744
745            } while (keepAlive);
746
747            log("SOCKET CLOSED");
748            s.close();
749        }
750
751        // Print string to log and output stream
752        void psPrint(PrintStream ps, String s) throws IOException {
753            log(s);
754            ps.print(s);
755        }
756
757        // Print bytes to log and output stream
758        void psWrite(PrintStream ps, byte[] bytes, int len) throws IOException {
759            log(new String(bytes));
760            ps.write(bytes, 0, len);
761        }
762
763        // Print CRLF to log and output stream
764        void psWriteEOL(PrintStream ps) throws IOException {
765            log("CRLF");
766            ps.write(EOL);
767        }
768
769
770        // Print status to log and output stream
771        void printStatus(PrintStream ps) throws IOException {
772            // Handle redirects first.
773            if (redirectCode != -1) {
774                log("REDIRECTING TO "+redirectHost+" status "+redirectCode);
775                psPrint(ps, HTTP_VERSION_STRING + " " + redirectCode +" Moved permanently");
776                psWriteEOL(ps);
777                psPrint(ps, "Location: " + redirectHost);
778                psWriteEOL(ps);
779                return;
780            }
781
782
783            if (testID.startsWith("test")) {
784                testNum = Integer.valueOf(testID.substring(4))-1;
785            }
786
787            if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) {
788                psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_NOT_FOUND + " not found");
789                psWriteEOL(ps);
790            }  else {
791                psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_OK+" OK");
792                psWriteEOL(ps);
793            }
794
795            log("Status sent");
796        }
797        /**
798         * Create the server response and output to the stream
799         * @param ps The PrintStream to output response headers and data to
800         */
801        void printHeaders(PrintStream ps) throws IOException {
802            psPrint(ps,"Server: TestWebServer"+mPort);
803            psWriteEOL(ps);
804            psPrint(ps, "Date: " + (new Date()));
805            psWriteEOL(ps);
806            psPrint(ps, "Connection: " + ((keepAlive) ? "Keep-Alive" : "Close"));
807            psWriteEOL(ps);
808
809            // Yuk, if we're not redirecting, we add the file details
810            if (redirectCode == -1) {
811
812                if (!TestWebData.testParams[testNum].testDir) {
813                    if (chunked) {
814                        psPrint(ps, "Transfer-Encoding: chunked");
815                    } else {
816                        psPrint(ps, "Content-length: "+TestWebData.testParams[testNum].testLength);
817                    }
818                    psWriteEOL(ps);
819
820                    psPrint(ps,"Last Modified: " + (new
821                                                    Date(TestWebData.testParams[testNum].testLastModified)));
822                    psWriteEOL(ps);
823
824                    psPrint(ps, "Content-type: " + TestWebData.testParams[testNum].testType);
825                    psWriteEOL(ps);
826                } else {
827                    psPrint(ps, "Content-type: text/html");
828                    psWriteEOL(ps);
829                }
830            } else {
831                // Content-length of 301, 302, 303, 307 are the same.
832                psPrint(ps, "Content-length: "+(TestWebData.testServerResponse[TestWebData.REDIRECT_301]).length());
833                psWriteEOL(ps);
834                psWriteEOL(ps);
835            }
836            log("Headers sent");
837
838        }
839
840        /**
841         * Sends the 404 not found message
842         * @param ps The PrintStream to write to
843         */
844        void send404(PrintStream ps) throws IOException {
845            ps.println("Not Found\n\n"+
846                       "The requested resource was not found.\n");
847        }
848
849        /**
850         * Sends the data associated with the headers
851         * @param ps The PrintStream to write to
852         */
853        void sendFile(PrintStream ps) throws IOException {
854            // For now just make a chunk with the whole of the test data
855            // It might be worth making this multiple chunks for large
856            // test data to test multiple chunks.
857            int dataSize = TestWebData.tests[testNum].length;
858            if (chunked) {
859                psPrint(ps, Integer.toHexString(dataSize));
860                psWriteEOL(ps);
861                psWrite(ps, TestWebData.tests[testNum], dataSize);
862                psWriteEOL(ps);
863                psPrint(ps, "0");
864                psWriteEOL(ps);
865                psWriteEOL(ps);
866            } else {
867                psWrite(ps, TestWebData.tests[testNum], dataSize);
868            }
869        }
870    }
871}
872