summaryrefslogtreecommitdiff
path: root/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java
blob: 4158fd31c1f830d21ba029e97ceea2c96fbb44d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package de.test.antennapod.util.service.download;

import android.util.Base64;
import android.util.Log;

import fi.iki.elonen.NanoHTTPD;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.GZIPOutputStream;

import de.danoeh.antennapod.BuildConfig;

/**
 * Http server for testing purposes
 * <p/>
 * Supported features:
 * <p/>
 * /status/code: Returns HTTP response with the given status code
 * /redirect/n:  Redirects n times
 * /delay/n:     Delay response for n seconds
 * /basic-auth/username/password: Basic auth with username and password
 * /gzip/n:      Send gzipped data of size n bytes
 * /files/id:     Accesses the file with the specified ID (this has to be added first via serveFile).
 */
public class HTTPBin extends NanoHTTPD {
    private static final String TAG = "HTTPBin";
    public static final int PORT = 8124;
    public static final String BASE_URL = "http://127.0.0.1:" + HTTPBin.PORT;


    private static final String MIME_HTML = "text/html";
    private static final String MIME_PLAIN = "text/plain";

    private final List<File> servedFiles;

    public HTTPBin() {
        super(PORT);
        this.servedFiles = new ArrayList<>();
    }

    /**
     * Adds the given file to the server.
     *
     * @return The ID of the file or -1 if the file could not be added to the server.
     */
    public synchronized int serveFile(File file) {
        if (file == null) throw new IllegalArgumentException("file = null");
        if (!file.exists()) {
            return -1;
        }
        for (int i = 0; i < servedFiles.size(); i++) {
            if (servedFiles.get(i).getAbsolutePath().equals(file.getAbsolutePath())) {
                return i;
            }
        }
        servedFiles.add(file);
        return servedFiles.size() - 1;
    }

    public synchronized File accessFile(int id) {
        if (id < 0 || id >= servedFiles.size()) {
            return null;
        } else {
            return servedFiles.get(id);
        }
    }

    @Override
    public Response serve(IHTTPSession session) {

        if (BuildConfig.DEBUG) Log.d(TAG, "Requested url: " + session.getUri());

        String[] segments = session.getUri().split("/");
        if (segments.length < 3) {
            Log.w(TAG, String.format("Invalid number of URI segments: %d %s", segments.length, Arrays.toString(segments)));
            get404Error();
        }

        final String func = segments[1];
        final String param = segments[2];
        final Map<String, String> headers = session.getHeaders();

        if (func.equalsIgnoreCase("status")) {
            try {
                int code = Integer.parseInt(param);
                return getStatus(code);
            } catch (NumberFormatException e) {
                e.printStackTrace();
                return getInternalError();
            }

        } else if (func.equalsIgnoreCase("redirect")) {
            try {
                int times = Integer.parseInt(param);
                if (times < 0) {
                    throw new NumberFormatException("times <= 0: " + times);
                }

                return getRedirectResponse(times - 1);
            } catch (NumberFormatException e) {
                e.printStackTrace();
                return getInternalError();
            }
        } else if (func.equalsIgnoreCase("delay")) {
            try {
                int sec = Integer.parseInt(param);
                if (sec <= 0) {
                    throw new NumberFormatException("sec <= 0: " + sec);
                }

                Thread.sleep(sec * 1000L);
                return getOKResponse();
            } catch (NumberFormatException e) {
                e.printStackTrace();
                return getInternalError();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return getInternalError();
            }
        } else if (func.equalsIgnoreCase("basic-auth")) {
            if (!headers.containsKey("authorization")) {
                Log.w(TAG, "No credentials provided");
                return getUnauthorizedResponse();
            }
            try {
                String credentials = new String(Base64.decode(headers.get("authorization").split(" ")[1], 0), "UTF-8");
                String[] credentialParts = credentials.split(":");
                if (credentialParts.length != 2) {
                    Log.w(TAG, "Unable to split credentials: " + Arrays.toString(credentialParts));
                    return getInternalError();
                }
                if (credentialParts[0].equals(segments[2])
                        && credentialParts[1].equals(segments[3])) {
                    Log.i(TAG, "Credentials accepted");
                    return getOKResponse();
                } else {
                    Log.w(TAG, String.format("Invalid credentials. Expected %s, %s, but was %s, %s",
                            segments[2], segments[3], credentialParts[0], credentialParts[1]));
                    return getUnauthorizedResponse();
                }

            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                return getInternalError();
            }
        } else if (func.equalsIgnoreCase("gzip")) {
            try {
                int size = Integer.parseInt(param);
                if (size <= 0) {
                    Log.w(TAG, "Invalid size for gzipped data: " + size);
                    throw new NumberFormatException();
                }

                return getGzippedResponse(size);
            } catch (NumberFormatException e) {
                e.printStackTrace();
                return getInternalError();
            } catch (IOException e) {
                e.printStackTrace();
                return getInternalError();
            }
        } else if (func.equalsIgnoreCase("files")) {
            try {
                int id = Integer.parseInt(param);
                if (id < 0) {
                    Log.w(TAG, "Invalid ID: " + id);
                    throw new NumberFormatException();
                }
                return getFileAccessResponse(id, headers);

            } catch (NumberFormatException e) {
                e.printStackTrace();
                return getInternalError();
            }
        }

        return get404Error();
    }

    private synchronized Response getFileAccessResponse(int id, Map<String, String> header) {
        File file = accessFile(id);
        if (file == null || !file.exists()) {
            Log.w(TAG, "File not found: " + id);
            return get404Error();
        }
        InputStream inputStream = null;
        String contentRange = null;
        Response.Status status;
        boolean successful = false;
        try {
            inputStream = new FileInputStream(file);
            if (header.containsKey("range")) {
                // read range header field
                final String value = header.get("range");
                final String[] segments = value.split("=");
                if (segments.length != 2) {
                    Log.w(TAG, "Invalid segment length: " + Arrays.toString(segments));
                    return getInternalError();
                }
                final String type = StringUtils.substringBefore(value, "=");
                if (!type.equalsIgnoreCase("bytes")) {
                    Log.w(TAG, "Range is not specified in bytes: " + value);
                    return getInternalError();
                }
                try {
                    long start = Long.parseLong(StringUtils.substringBefore(segments[1], "-"));
                    if (start >= file.length()) {
                        return getRangeNotSatisfiable();
                    }

                    // skip 'start' bytes
                    IOUtils.skipFully(inputStream, start);
                    contentRange = "bytes " + start + (file.length() - 1) + "/" + file.length();

                } catch (NumberFormatException e) {
                    e.printStackTrace();
                    return getInternalError();
                } catch (IOException e) {
                    e.printStackTrace();
                    return getInternalError();
                }

                status = Response.Status.PARTIAL_CONTENT;

            } else {
                // request did not contain range header field
                status = Response.Status.OK;
            }
            successful = true;
        } catch (FileNotFoundException e) {
            e.printStackTrace();

            return getInternalError();
        } finally {
            if (!successful && inputStream != null) {
                IOUtils.closeQuietly(inputStream);
            }
        }

        Response response = new Response(status, URLConnection.guessContentTypeFromName(file.getAbsolutePath()), inputStream);

        response.addHeader("Accept-Ranges", "bytes");
        if (contentRange != null) {
            response.addHeader("Content-Range", contentRange);
        }
        response.addHeader("Content-Length", String.valueOf(file.length()));
        return response;
    }

    private Response getGzippedResponse(int size) throws IOException {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        final byte[] buffer = new byte[size];
        Random random = new Random(System.currentTimeMillis());
        random.nextBytes(buffer);

        ByteArrayOutputStream compressed = new ByteArrayOutputStream(buffer.length);
        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressed);
        gzipOutputStream.write(buffer);
        gzipOutputStream.close();

        InputStream inputStream = new ByteArrayInputStream(compressed.toByteArray());
        Response response = new Response(Response.Status.OK, MIME_PLAIN, inputStream);
        response.addHeader("Content-Encoding", "gzip");
        response.addHeader("Content-Length", String.valueOf(compressed.size()));
        return response;
    }

    private Response getStatus(final int code) {
        Response.IStatus status = (code == 200) ? Response.Status.OK :
                (code == 201) ? Response.Status.CREATED :
                        (code == 206) ? Response.Status.PARTIAL_CONTENT :
                                (code == 301) ? Response.Status.REDIRECT :
                                        (code == 304) ? Response.Status.NOT_MODIFIED :
                                                (code == 400) ? Response.Status.BAD_REQUEST :
                                                        (code == 401) ? Response.Status.UNAUTHORIZED :
                                                                (code == 403) ? Response.Status.FORBIDDEN :
                                                                        (code == 404) ? Response.Status.NOT_FOUND :
                                                                                (code == 405) ? Response.Status.METHOD_NOT_ALLOWED :
                                                                                        (code == 416) ? Response.Status.RANGE_NOT_SATISFIABLE :
                                                                                                (code == 500) ? Response.Status.INTERNAL_ERROR : new Response.IStatus() {
                                                                                                    @Override
                                                                                                    public int getRequestStatus() {
                                                                                                        return code;
                                                                                                    }

                                                                                                    @Override
                                                                                                    public String getDescription() {
                                                                                                        return "Unknown";
                                                                                                    }
                                                                                                };
        return new Response(status, MIME_HTML, "");

    }

    private Response getRedirectResponse(int times) {
        if (times > 0) {
            Response response = new Response(Response.Status.REDIRECT, MIME_HTML, "This resource has been moved permanently");
            response.addHeader("Location", "/redirect/" + times);
            return response;
        } else if (times == 0) {
            return getOKResponse();
        } else {
            return getInternalError();
        }
    }

    private Response getUnauthorizedResponse() {
        Response response = new Response(Response.Status.UNAUTHORIZED, MIME_HTML, "");
        response.addHeader("WWW-Authenticate", "Basic realm=\"Test Realm\"");
        return response;
    }

    private Response getOKResponse() {
        return new Response(Response.Status.OK, MIME_HTML, "");
    }

    private Response getInternalError() {
        return new Response(Response.Status.INTERNAL_ERROR, MIME_HTML, "The server encountered an internal error");
    }

    private Response getRangeNotSatisfiable() {
        return new Response(Response.Status.RANGE_NOT_SATISFIABLE, MIME_PLAIN, "");
    }

    private Response get404Error() {
        return new Response(Response.Status.NOT_FOUND, MIME_HTML, "The requested URL was not found on this server");
    }
}