It's possible for data->sock to get torn down in tcp error handling. If we unconditionally tear it down again we will end up doing writes to an offset of the NULL pointer when we go to tear it down again. Detect if it has been torn down and don't do it again. Signed-off-by: Daniel Axtens <dja@axtens.net> Reviewed-by: Daniel Kiper <daniel.kiper@oracle.com>
600 lines
15 KiB
C
600 lines
15 KiB
C
/*
|
|
* GRUB -- GRand Unified Bootloader
|
|
* Copyright (C) 2010,2011 Free Software Foundation, Inc.
|
|
*
|
|
* GRUB is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* GRUB is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with GRUB. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <grub/misc.h>
|
|
#include <grub/net/tcp.h>
|
|
#include <grub/net/ip.h>
|
|
#include <grub/net/ethernet.h>
|
|
#include <grub/net/netbuff.h>
|
|
#include <grub/net.h>
|
|
#include <grub/mm.h>
|
|
#include <grub/dl.h>
|
|
#include <grub/file.h>
|
|
#include <grub/i18n.h>
|
|
|
|
GRUB_MOD_LICENSE ("GPLv3+");
|
|
|
|
enum
|
|
{
|
|
HTTP_PORT = 80
|
|
};
|
|
|
|
|
|
typedef struct http_data
|
|
{
|
|
char *current_line;
|
|
grub_size_t current_line_len;
|
|
int headers_recv;
|
|
int first_line_recv;
|
|
int size_recv;
|
|
grub_net_tcp_socket_t sock;
|
|
char *filename;
|
|
grub_err_t err;
|
|
char *errmsg;
|
|
int chunked;
|
|
grub_size_t chunk_rem;
|
|
int in_chunk_len;
|
|
} *http_data_t;
|
|
|
|
static grub_off_t
|
|
have_ahead (struct grub_file *file)
|
|
{
|
|
grub_net_t net = file->device->net;
|
|
grub_off_t ret = net->offset;
|
|
struct grub_net_packet *pack;
|
|
for (pack = net->packs.first; pack; pack = pack->next)
|
|
ret += pack->nb->tail - pack->nb->data;
|
|
return ret;
|
|
}
|
|
|
|
static grub_err_t
|
|
parse_line (grub_file_t file, http_data_t data, char *ptr, grub_size_t len)
|
|
{
|
|
char *end = ptr + len;
|
|
while (end > ptr && *(end - 1) == '\r')
|
|
end--;
|
|
*end = 0;
|
|
/* Trailing CRLF. */
|
|
if (data->in_chunk_len == 1)
|
|
{
|
|
data->in_chunk_len = 2;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
if (data->in_chunk_len == 2)
|
|
{
|
|
data->chunk_rem = grub_strtoul (ptr, 0, 16);
|
|
grub_errno = GRUB_ERR_NONE;
|
|
if (data->chunk_rem == 0)
|
|
{
|
|
file->device->net->eof = 1;
|
|
file->device->net->stall = 1;
|
|
if (file->size == GRUB_FILE_SIZE_UNKNOWN)
|
|
file->size = have_ahead (file);
|
|
}
|
|
data->in_chunk_len = 0;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
if (ptr == end)
|
|
{
|
|
data->headers_recv = 1;
|
|
if (data->chunked)
|
|
data->in_chunk_len = 2;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
if (!data->first_line_recv)
|
|
{
|
|
int code;
|
|
if (grub_memcmp (ptr, "HTTP/1.1 ", sizeof ("HTTP/1.1 ") - 1) != 0)
|
|
{
|
|
data->errmsg = grub_strdup (_("unsupported HTTP response"));
|
|
data->first_line_recv = 1;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
ptr += sizeof ("HTTP/1.1 ") - 1;
|
|
code = grub_strtoul (ptr, (const char **)&ptr, 10);
|
|
if (grub_errno)
|
|
return grub_errno;
|
|
switch (code)
|
|
{
|
|
case 200:
|
|
case 206:
|
|
break;
|
|
case 404:
|
|
data->err = GRUB_ERR_FILE_NOT_FOUND;
|
|
data->errmsg = grub_xasprintf (_("file `%s' not found"), data->filename);
|
|
return GRUB_ERR_NONE;
|
|
default:
|
|
data->err = GRUB_ERR_NET_UNKNOWN_ERROR;
|
|
/* TRANSLATORS: GRUB HTTP code is pretty young. So even perfectly
|
|
valid answers like 403 will trigger this very generic message. */
|
|
data->errmsg = grub_xasprintf (_("unsupported HTTP error %d: %s"),
|
|
code, ptr);
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
data->first_line_recv = 1;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
if (grub_memcmp (ptr, "Content-Length: ", sizeof ("Content-Length: ") - 1)
|
|
== 0 && !data->size_recv)
|
|
{
|
|
ptr += sizeof ("Content-Length: ") - 1;
|
|
file->size = grub_strtoull (ptr, (const char **)&ptr, 10);
|
|
data->size_recv = 1;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
if (grub_memcmp (ptr, "Transfer-Encoding: chunked",
|
|
sizeof ("Transfer-Encoding: chunked") - 1) == 0)
|
|
{
|
|
data->chunked = 1;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
static void
|
|
http_err (grub_net_tcp_socket_t sock __attribute__ ((unused)),
|
|
void *f)
|
|
{
|
|
grub_file_t file = f;
|
|
http_data_t data = file->data;
|
|
|
|
if (data->sock)
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
data->sock = 0;
|
|
if (data->current_line)
|
|
grub_free (data->current_line);
|
|
data->current_line = 0;
|
|
file->device->net->eof = 1;
|
|
file->device->net->stall = 1;
|
|
if (file->size == GRUB_FILE_SIZE_UNKNOWN)
|
|
file->size = have_ahead (file);
|
|
}
|
|
|
|
static grub_err_t
|
|
http_receive (grub_net_tcp_socket_t sock __attribute__ ((unused)),
|
|
struct grub_net_buff *nb,
|
|
void *f)
|
|
{
|
|
grub_file_t file = f;
|
|
http_data_t data = file->data;
|
|
grub_err_t err;
|
|
|
|
if (!data->sock)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
while (1)
|
|
{
|
|
char *ptr = (char *) nb->data;
|
|
if ((!data->headers_recv || data->in_chunk_len) && data->current_line)
|
|
{
|
|
int have_line = 1;
|
|
char *t;
|
|
ptr = grub_memchr (nb->data, '\n', nb->tail - nb->data);
|
|
if (ptr)
|
|
ptr++;
|
|
else
|
|
{
|
|
have_line = 0;
|
|
ptr = (char *) nb->tail;
|
|
}
|
|
t = grub_realloc (data->current_line,
|
|
data->current_line_len + (ptr - (char *) nb->data));
|
|
if (!t)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
return grub_errno;
|
|
}
|
|
|
|
data->current_line = t;
|
|
grub_memcpy (data->current_line + data->current_line_len,
|
|
nb->data, ptr - (char *) nb->data);
|
|
data->current_line_len += ptr - (char *) nb->data;
|
|
if (!have_line)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
err = parse_line (file, data, data->current_line,
|
|
data->current_line_len);
|
|
grub_free (data->current_line);
|
|
data->current_line = 0;
|
|
data->current_line_len = 0;
|
|
if (err)
|
|
{
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
}
|
|
|
|
while (ptr < (char *) nb->tail && (!data->headers_recv
|
|
|| data->in_chunk_len))
|
|
{
|
|
char *ptr2;
|
|
ptr2 = grub_memchr (ptr, '\n', (char *) nb->tail - ptr);
|
|
if (!ptr2)
|
|
{
|
|
data->current_line = grub_malloc ((char *) nb->tail - ptr);
|
|
if (!data->current_line)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
return grub_errno;
|
|
}
|
|
data->current_line_len = (char *) nb->tail - ptr;
|
|
grub_memcpy (data->current_line, ptr, data->current_line_len);
|
|
grub_netbuff_free (nb);
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
err = parse_line (file, data, ptr, ptr2 - ptr);
|
|
if (err)
|
|
{
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
ptr = ptr2 + 1;
|
|
}
|
|
|
|
if (((char *) nb->tail - ptr) <= 0)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
err = grub_netbuff_pull (nb, ptr - (char *) nb->data);
|
|
if (err)
|
|
{
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
if (!(data->chunked && (grub_ssize_t) data->chunk_rem
|
|
< nb->tail - nb->data))
|
|
{
|
|
grub_net_put_packet (&file->device->net->packs, nb);
|
|
if (file->device->net->packs.count >= 20)
|
|
file->device->net->stall = 1;
|
|
|
|
if (file->device->net->packs.count >= 100)
|
|
grub_net_tcp_stall (data->sock);
|
|
|
|
if (data->chunked)
|
|
data->chunk_rem -= nb->tail - nb->data;
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
if (data->chunk_rem)
|
|
{
|
|
struct grub_net_buff *nb2;
|
|
nb2 = grub_netbuff_alloc (data->chunk_rem);
|
|
if (!nb2)
|
|
return grub_errno;
|
|
grub_netbuff_put (nb2, data->chunk_rem);
|
|
grub_memcpy (nb2->data, nb->data, data->chunk_rem);
|
|
if (file->device->net->packs.count >= 20)
|
|
{
|
|
file->device->net->stall = 1;
|
|
grub_net_tcp_stall (data->sock);
|
|
}
|
|
|
|
grub_net_put_packet (&file->device->net->packs, nb2);
|
|
grub_netbuff_pull (nb, data->chunk_rem);
|
|
}
|
|
data->in_chunk_len = 1;
|
|
}
|
|
}
|
|
|
|
static grub_err_t
|
|
http_establish (struct grub_file *file, grub_off_t offset, int initial)
|
|
{
|
|
http_data_t data = file->data;
|
|
grub_uint8_t *ptr;
|
|
int i;
|
|
struct grub_net_buff *nb;
|
|
grub_err_t err;
|
|
char *server_name;
|
|
char *port_string;
|
|
const char *port_string_end;
|
|
unsigned long port_number;
|
|
|
|
nb = grub_netbuff_alloc (GRUB_NET_TCP_RESERVE_SIZE
|
|
+ sizeof ("GET ") - 1
|
|
+ grub_strlen (data->filename)
|
|
+ sizeof (" HTTP/1.1\r\nHost: ") - 1
|
|
+ grub_strlen (file->device->net->server)
|
|
+ sizeof ("\r\nUser-Agent: " PACKAGE_STRING
|
|
"\r\n") - 1
|
|
+ sizeof ("Range: bytes=XXXXXXXXXXXXXXXXXXXX"
|
|
"-\r\n\r\n"));
|
|
if (!nb)
|
|
return grub_errno;
|
|
|
|
grub_netbuff_reserve (nb, GRUB_NET_TCP_RESERVE_SIZE);
|
|
ptr = nb->tail;
|
|
err = grub_netbuff_put (nb, sizeof ("GET ") - 1);
|
|
if (err)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
grub_memcpy (ptr, "GET ", sizeof ("GET ") - 1);
|
|
|
|
ptr = nb->tail;
|
|
|
|
err = grub_netbuff_put (nb, grub_strlen (data->filename));
|
|
if (err)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
grub_memcpy (ptr, data->filename, grub_strlen (data->filename));
|
|
|
|
ptr = nb->tail;
|
|
err = grub_netbuff_put (nb, sizeof (" HTTP/1.1\r\nHost: ") - 1);
|
|
if (err)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
grub_memcpy (ptr, " HTTP/1.1\r\nHost: ",
|
|
sizeof (" HTTP/1.1\r\nHost: ") - 1);
|
|
|
|
ptr = nb->tail;
|
|
err = grub_netbuff_put (nb, grub_strlen (file->device->net->server));
|
|
if (err)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
grub_memcpy (ptr, file->device->net->server,
|
|
grub_strlen (file->device->net->server));
|
|
|
|
ptr = nb->tail;
|
|
err = grub_netbuff_put (nb,
|
|
sizeof ("\r\nUser-Agent: " PACKAGE_STRING "\r\n")
|
|
- 1);
|
|
if (err)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return err;
|
|
}
|
|
grub_memcpy (ptr, "\r\nUser-Agent: " PACKAGE_STRING "\r\n",
|
|
sizeof ("\r\nUser-Agent: " PACKAGE_STRING "\r\n") - 1);
|
|
if (!initial)
|
|
{
|
|
ptr = nb->tail;
|
|
grub_snprintf ((char *) ptr,
|
|
sizeof ("Range: bytes=XXXXXXXXXXXXXXXXXXXX-"
|
|
"\r\n"),
|
|
"Range: bytes=%" PRIuGRUB_UINT64_T "-\r\n",
|
|
offset);
|
|
grub_netbuff_put (nb, grub_strlen ((char *) ptr));
|
|
}
|
|
ptr = nb->tail;
|
|
grub_netbuff_put (nb, 2);
|
|
grub_memcpy (ptr, "\r\n", 2);
|
|
|
|
port_string = grub_strrchr (file->device->net->server, ',');
|
|
if (port_string == NULL)
|
|
{
|
|
/* If ",port" is not found in the http server string, look for ":port". */
|
|
port_string = grub_strrchr (file->device->net->server, ':');
|
|
/* For IPv6 addresses, the ":port" syntax is not supported and ",port" must be used. */
|
|
if (port_string != NULL && grub_strchr (file->device->net->server, ':') != port_string)
|
|
port_string = NULL;
|
|
}
|
|
if (port_string != NULL)
|
|
{
|
|
port_number = grub_strtoul (port_string + 1, &port_string_end, 10);
|
|
if (*(port_string + 1) == '\0' || *port_string_end != '\0')
|
|
return grub_error (GRUB_ERR_BAD_NUMBER, N_("non-numeric or invalid port number `%s'"), port_string + 1);
|
|
if (port_number == 0 || port_number > 65535)
|
|
return grub_error (GRUB_ERR_OUT_OF_RANGE, N_("port number `%s' not in the range of 1 to 65535"), port_string + 1);
|
|
|
|
server_name = grub_strdup (file->device->net->server);
|
|
if (server_name == NULL)
|
|
return grub_errno;
|
|
server_name[port_string - file->device->net->server] = '\0';
|
|
}
|
|
else
|
|
{
|
|
port_number = HTTP_PORT;
|
|
server_name = file->device->net->server;
|
|
}
|
|
|
|
data->sock = grub_net_tcp_open (server_name,
|
|
port_number, http_receive,
|
|
http_err, NULL,
|
|
file);
|
|
|
|
if (server_name != file->device->net->server)
|
|
grub_free (server_name);
|
|
|
|
if (!data->sock)
|
|
{
|
|
grub_netbuff_free (nb);
|
|
return grub_errno;
|
|
}
|
|
|
|
// grub_net_poll_cards (5000);
|
|
|
|
err = grub_net_send_tcp_packet (data->sock, nb, 1);
|
|
if (err)
|
|
{
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
return err;
|
|
}
|
|
|
|
for (i = 0; data->sock && !data->headers_recv && i < 100; i++)
|
|
{
|
|
grub_net_tcp_retransmit ();
|
|
grub_net_poll_cards (300, &data->headers_recv);
|
|
}
|
|
|
|
if (!data->headers_recv)
|
|
{
|
|
if (data->sock)
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
if (data->err)
|
|
{
|
|
char *str = data->errmsg;
|
|
err = grub_error (data->err, "%s", str);
|
|
grub_free (str);
|
|
data->errmsg = 0;
|
|
return data->err;
|
|
}
|
|
return grub_error (GRUB_ERR_TIMEOUT, N_("time out opening `%s'"), data->filename);
|
|
}
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
static grub_err_t
|
|
http_seek (struct grub_file *file, grub_off_t off)
|
|
{
|
|
struct http_data *old_data, *data;
|
|
grub_err_t err;
|
|
old_data = file->data;
|
|
/* FIXME: Reuse socket? */
|
|
if (old_data->sock)
|
|
grub_net_tcp_close (old_data->sock, GRUB_NET_TCP_ABORT);
|
|
old_data->sock = 0;
|
|
|
|
while (file->device->net->packs.first)
|
|
{
|
|
grub_netbuff_free (file->device->net->packs.first->nb);
|
|
grub_net_remove_packet (file->device->net->packs.first);
|
|
}
|
|
|
|
file->device->net->stall = 0;
|
|
file->device->net->eof = 0;
|
|
file->device->net->offset = off;
|
|
|
|
data = grub_zalloc (sizeof (*data));
|
|
if (!data)
|
|
return grub_errno;
|
|
|
|
data->size_recv = 1;
|
|
data->filename = old_data->filename;
|
|
if (!data->filename)
|
|
{
|
|
grub_free (data);
|
|
file->data = 0;
|
|
return grub_errno;
|
|
}
|
|
grub_free (old_data);
|
|
|
|
file->data = data;
|
|
err = http_establish (file, off, 0);
|
|
if (err)
|
|
{
|
|
grub_free (data->filename);
|
|
grub_free (data);
|
|
file->data = 0;
|
|
return err;
|
|
}
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
static grub_err_t
|
|
http_open (struct grub_file *file, const char *filename)
|
|
{
|
|
grub_err_t err;
|
|
struct http_data *data;
|
|
|
|
data = grub_zalloc (sizeof (*data));
|
|
if (!data)
|
|
return grub_errno;
|
|
file->size = GRUB_FILE_SIZE_UNKNOWN;
|
|
|
|
data->filename = grub_strdup (filename);
|
|
if (!data->filename)
|
|
{
|
|
grub_free (data);
|
|
return grub_errno;
|
|
}
|
|
|
|
file->not_easily_seekable = 0;
|
|
file->data = data;
|
|
|
|
err = http_establish (file, 0, 1);
|
|
if (err)
|
|
{
|
|
grub_free (data->filename);
|
|
grub_free (data);
|
|
return err;
|
|
}
|
|
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
static grub_err_t
|
|
http_close (struct grub_file *file)
|
|
{
|
|
http_data_t data = file->data;
|
|
|
|
if (!data)
|
|
return GRUB_ERR_NONE;
|
|
|
|
if (data->sock)
|
|
grub_net_tcp_close (data->sock, GRUB_NET_TCP_ABORT);
|
|
if (data->current_line)
|
|
grub_free (data->current_line);
|
|
grub_free (data->filename);
|
|
grub_free (data);
|
|
return GRUB_ERR_NONE;
|
|
}
|
|
|
|
static grub_err_t
|
|
http_packets_pulled (struct grub_file *file)
|
|
{
|
|
http_data_t data = file->data;
|
|
|
|
if (file->device->net->packs.count >= 20)
|
|
return 0;
|
|
|
|
if (!file->device->net->eof)
|
|
file->device->net->stall = 0;
|
|
if (data && data->sock)
|
|
grub_net_tcp_unstall (data->sock);
|
|
return 0;
|
|
}
|
|
|
|
static struct grub_net_app_protocol grub_http_protocol =
|
|
{
|
|
.name = "http",
|
|
.open = http_open,
|
|
.close = http_close,
|
|
.seek = http_seek,
|
|
.packets_pulled = http_packets_pulled
|
|
};
|
|
|
|
GRUB_MOD_INIT (http)
|
|
{
|
|
grub_net_app_level_register (&grub_http_protocol);
|
|
}
|
|
|
|
GRUB_MOD_FINI (http)
|
|
{
|
|
grub_net_app_level_unregister (&grub_http_protocol);
|
|
}
|