httpstream: reintroduce d318d544cf76ea58c4f12bccc0541d888af7570c:
[openblackhole/openblackhole-enigma2.git] / lib / base / httpstream.cpp
1 #include <cstdio>
2 #include <openssl/evp.h>
3
4 #include <lib/base/httpstream.h>
5 #include <lib/base/eerror.h>
6 #include <lib/base/wrappers.h>
7
8 DEFINE_REF(eHttpStream);
9
10 eHttpStream::eHttpStream()
11 {
12         streamSocket = -1;
13         connectionStatus = FAILED;
14         isChunked = false;
15         currentChunkSize = 0;
16         partialPktSz = 0;
17         tmpBufSize = 32;
18         tmpBuf = (char*)malloc(tmpBufSize);
19 }
20
21 eHttpStream::~eHttpStream()
22 {
23         free(tmpBuf);
24         kill(true);
25         close();
26 }
27
28 int eHttpStream::openUrl(const std::string &url, std::string &newurl)
29 {
30         int port;
31         std::string hostname;
32         std::string uri = url;
33         std::string request;
34         size_t buflen = 1024;
35         char *linebuf = NULL;
36         int result;
37         char proto[100];
38         int statuscode = 0;
39         char statusmsg[100];
40         bool playlist = false;
41         bool contenttypeparsed = false;
42
43         close();
44
45         int pathindex = uri.find("/", 7);
46         if (pathindex > 0)
47         {
48                 hostname = uri.substr(7, pathindex - 7);
49                 uri = uri.substr(pathindex, uri.length() - pathindex);
50         }
51         else
52         {
53                 hostname = uri.substr(7, uri.length() - 7);
54                 uri = "/";
55         }
56         int authenticationindex = hostname.find("@");
57         if (authenticationindex > 0)
58         {
59                 BIO *mbio, *b64bio, *bio;
60                 char *p = (char*)NULL;
61                 int length = 0;
62                 authorizationData = hostname.substr(0, authenticationindex);
63                 hostname = hostname.substr(authenticationindex + 1);
64                 mbio = BIO_new(BIO_s_mem());
65                 b64bio = BIO_new(BIO_f_base64());
66                 bio = BIO_push(b64bio, mbio);
67                 BIO_write(bio, authorizationData.c_str(), authorizationData.length());
68                 BIO_flush(bio);
69                 length = BIO_ctrl(mbio, BIO_CTRL_INFO, 0, (char*)&p);
70                 authorizationData = "";
71                 if (p && length > 0)
72                 {
73                         /* base64 output contains a linefeed, which we ignore */
74                         authorizationData.append(p, length - 1);
75                 }
76                 BIO_free_all(bio);
77         }
78         int customportindex = hostname.find(":");
79         if (customportindex > 0)
80         {
81                 port = atoi(hostname.substr(customportindex + 1, hostname.length() - customportindex - 1).c_str());
82                 hostname = hostname.substr(0, customportindex);
83         }
84         else if (customportindex == 0)
85         {
86                 port = atoi(hostname.substr(1, hostname.length() - 1).c_str());
87                 hostname = "localhost";
88         }
89         else
90         {
91                 port = 80;
92         }
93         streamSocket = Connect(hostname.c_str(), port, 10);
94         if (streamSocket < 0)
95                 goto error;
96
97         request = "GET ";
98         request.append(uri).append(" HTTP/1.1\r\n");
99         request.append("Host: ").append(hostname).append("\r\n");
100         request.append("User-Agent: ").append("Enigma2").append("\r\n");
101         if (authorizationData != "")
102         {
103                 request.append("Authorization: Basic ").append(authorizationData).append("\r\n");
104         }
105         request.append("Accept: */*\r\n");
106         request.append("Connection: close\r\n");
107         request.append("\r\n");
108
109         writeAll(streamSocket, request.c_str(), request.length());
110
111         linebuf = (char*)malloc(buflen);
112
113         result = readLine(streamSocket, &linebuf, &buflen);
114         if (result <= 0)
115                 goto error;
116
117         result = sscanf(linebuf, "%99s %d %99s", proto, &statuscode, statusmsg);
118         if (result != 3 || (statuscode != 200 && statuscode != 206 && statuscode != 302))
119         {
120                 eDebug("%s: wrong http response code: %d", __FUNCTION__, statuscode);
121                 goto error;
122         }
123
124         while (1)
125         {
126                 result = readLine(streamSocket, &linebuf, &buflen);
127                 if (!contenttypeparsed)
128                 {
129                         char contenttype[33];
130                         if (sscanf(linebuf, "Content-Type: %32s", contenttype) == 1)
131                         {
132                                 contenttypeparsed = true;
133                                 if (!strcasecmp(contenttype, "application/text")
134                                 || !strcasecmp(contenttype, "audio/x-mpegurl")
135                                 || !strcasecmp(contenttype, "audio/mpegurl")
136                                 || !strcasecmp(contenttype, "application/m3u"))
137                                 {
138                                         /* assume we'll get a playlist, some text file containing a stream url */
139                                         playlist = true;
140                                 }
141                                 continue;
142                         }
143                 }
144                 if (playlist && !strncasecmp(linebuf, "http://", 7))
145                 {
146                         newurl = linebuf;
147                         eDebug("%s: playlist entry: %s", __FUNCTION__, newurl.c_str());
148                         break;
149                 }
150                 if (statuscode == 302 && strncasecmp(linebuf, "location: ", 10) == 0)
151                 {
152                         newurl = &linebuf[10];
153                         eDebug("%s: redirecting to: %s", __FUNCTION__, newurl.c_str());
154                         break;
155                 }
156
157                 if (((statuscode == 200) || (statuscode == 206)) && !strncasecmp(linebuf, "transfer-encoding: chunked", strlen("transfer-encoding: chunked")))
158                 {
159                         isChunked = true;
160                 }
161                 if (!playlist && result == 0)
162                         break;
163                 if (result < 0)
164                         break;
165         }
166
167         free(linebuf);
168         return 0;
169 error:
170         eDebug("%s failed", __FUNCTION__);
171         free(linebuf);
172         close();
173         return -1;
174 }
175
176 int eHttpStream::open(const char *url)
177 {
178         streamUrl = url;
179         /*
180          * We're in gui thread context here, and establishing
181          * a connection might block for up to 10 seconds.
182          * Spawn a new thread to establish the connection.
183          */
184         connectionStatus = BUSY;
185         eDebug("eHttpStream::Start thread");
186         run();
187         return 0;
188 }
189
190 void eHttpStream::thread()
191 {
192         hasStarted();
193         std::string currenturl, newurl;
194         currenturl = streamUrl;
195         for (unsigned int i = 0; i < 5; i++)
196         {
197                 if (openUrl(currenturl, newurl) < 0)
198                 {
199                         /* connection failed */
200                         eDebug("eHttpStream::Thread end NO connection");
201                         connectionStatus = FAILED;
202                         return;
203                 }
204                 if (newurl == "")
205                 {
206                         /* we have a valid stream connection */
207                         eDebug("eHttpStream::Thread end connection");
208                         connectionStatus = CONNECTED;
209                         return;
210                 }
211                 /* switch to new url */
212                 close();
213                 currenturl = newurl;
214                 newurl = "";
215         }
216         /* too many redirect / playlist levels */
217         eDebug("eHttpStream::Thread end NO connection");
218         connectionStatus = FAILED;
219         return;
220 }
221
222 int eHttpStream::close()
223 {
224         int retval = -1;
225         if (streamSocket >= 0)
226         {
227                 retval = ::close(streamSocket);
228                 streamSocket = -1;
229         }
230         return retval;
231 }
232
233 ssize_t eHttpStream::syncNextRead(void *buf, ssize_t length)
234 {
235         unsigned char *b = (unsigned char*)buf;
236         unsigned char *e = b + length;
237         partialPktSz = 0;
238
239         if (*(char*)buf != 0x47)
240         {
241                 // the current read is not aligned
242                 // get the head position of the last packet
243                 // so we'll try to align the next read
244                 while (e != b && *e != 0x47) e--;
245         }
246         else
247         {
248                 // the current read is aligned
249                 // get the last incomplete packet position
250                 e -= length % packetSize;
251         }
252
253         if (e != b && e != (b + length))
254         {
255                 partialPktSz = (b + length) - e;
256                 // if the last packet is read partially save it to align the next read
257                 if (partialPktSz > 0 && partialPktSz < packetSize)
258                 {
259                         memcpy(partialPkt, e, partialPktSz);
260                 }
261         }
262         return (length - partialPktSz);
263 }
264
265 ssize_t eHttpStream::httpChunkedRead(void *buf, size_t count)
266 {
267         ssize_t ret = -1;
268         size_t total_read = partialPktSz;
269
270         // write partial packet from the previous read
271         if (partialPktSz > 0)
272         {
273                 memcpy(buf, partialPkt, partialPktSz);
274                 partialPktSz = 0;
275         }
276
277         if (!isChunked)
278         {
279                 ret = timedRead(streamSocket,((char*)buf) + total_read , count - total_read, 5000, 100);
280                 if (ret > 0)
281                 {
282                         ret += total_read;
283                         ret = syncNextRead(buf, ret);
284                 }
285         }
286         else
287         {
288                 while (total_read < count)
289                 {
290                         if (0 == currentChunkSize)
291                         {
292                                 do
293                                 {
294                                         ret = readLine(streamSocket, &tmpBuf, &tmpBufSize);
295                                         if (ret < 0) return -1;
296                                 } while (!*tmpBuf && ret > 0); /* skip CR LF from last chunk */
297                                 if (ret == 0)
298                                         break;
299                                 currentChunkSize = strtol(tmpBuf, NULL, 16);
300                                 if (currentChunkSize == 0) return -1;
301                         }
302
303                         size_t to_read = count - total_read;
304                         if (currentChunkSize < to_read)
305                                 to_read = currentChunkSize;
306
307                         // do not wait too long if we have something in the buffer already
308                         ret = timedRead(streamSocket, ((char*)buf) + total_read, to_read, ((total_read)? 100 : 5000), 100);
309                         if (ret <= 0)
310                                 break;
311                         currentChunkSize -= ret;
312                         total_read += ret;
313                 }
314                 if (total_read > 0)
315                 {
316                         ret = syncNextRead(buf, total_read);
317                 }
318         }
319         return ret;
320 }
321
322 ssize_t eHttpStream::read(off_t offset, void *buf, size_t count)
323 {
324         if (connectionStatus == BUSY)
325                 return 0;
326         else if (connectionStatus == FAILED)
327                 return -1;
328         return httpChunkedRead(buf, count);
329 }
330
331 int eHttpStream::valid()
332 {
333         if (connectionStatus == BUSY)
334                 return 0;
335         return streamSocket >= 0;
336 }
337
338 off_t eHttpStream::length()
339 {
340         return (off_t)-1;
341 }
342
343 off_t eHttpStream::offset()
344 {
345         return 0;
346 }