| /* |
| * 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/udp.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/priority_queue.h> |
| #include <grub/i18n.h> |
| |
| GRUB_MOD_LICENSE ("GPLv3+"); |
| |
| /* IP port for the MTFTP server used for Intel's PXE */ |
| enum |
| { |
| MTFTP_SERVER_PORT = 75, |
| MTFTP_CLIENT_PORT = 76, |
| /* IP port for the TFTP server */ |
| TFTP_SERVER_PORT = 69 |
| }; |
| |
| enum |
| { |
| TFTP_DEFAULTSIZE_PACKET = 512, |
| }; |
| |
| enum |
| { |
| TFTP_CODE_EOF = 1, |
| TFTP_CODE_MORE = 2, |
| TFTP_CODE_ERROR = 3, |
| TFTP_CODE_BOOT = 4, |
| TFTP_CODE_CFG = 5 |
| }; |
| |
| enum |
| { |
| TFTP_RRQ = 1, |
| TFTP_WRQ = 2, |
| TFTP_DATA = 3, |
| TFTP_ACK = 4, |
| TFTP_ERROR = 5, |
| TFTP_OACK = 6 |
| }; |
| |
| enum |
| { |
| TFTP_EUNDEF = 0, /* not defined */ |
| TFTP_ENOTFOUND = 1, /* file not found */ |
| TFTP_EACCESS = 2, /* access violation */ |
| TFTP_ENOSPACE = 3, /* disk full or allocation exceeded */ |
| TFTP_EBADOP = 4, /* illegal TFTP operation */ |
| TFTP_EBADID = 5, /* unknown transfer ID */ |
| TFTP_EEXISTS = 6, /* file already exists */ |
| TFTP_ENOUSER = 7 /* no such user */ |
| }; |
| |
| struct tftphdr { |
| grub_uint16_t opcode; |
| union { |
| grub_int8_t rrq[TFTP_DEFAULTSIZE_PACKET]; |
| struct { |
| grub_uint16_t block; |
| grub_int8_t download[0]; |
| } data; |
| struct { |
| grub_uint16_t block; |
| } ack; |
| struct { |
| grub_uint16_t errcode; |
| grub_int8_t errmsg[TFTP_DEFAULTSIZE_PACKET]; |
| } err; |
| struct { |
| grub_int8_t data[TFTP_DEFAULTSIZE_PACKET+2]; |
| } oack; |
| } u; |
| } GRUB_PACKED ; |
| |
| |
| typedef struct tftp_data |
| { |
| grub_uint64_t file_size; |
| grub_uint64_t block; |
| grub_uint32_t block_size; |
| grub_uint64_t ack_sent; |
| int have_oack; |
| struct grub_error_saved save_err; |
| grub_net_udp_socket_t sock; |
| grub_priority_queue_t pq; |
| } *tftp_data_t; |
| |
| static int |
| cmp_block (grub_uint16_t a, grub_uint16_t b) |
| { |
| grub_int16_t i = (grub_int16_t) (a - b); |
| if (i > 0) |
| return +1; |
| if (i < 0) |
| return -1; |
| return 0; |
| } |
| |
| static int |
| cmp (const void *a__, const void *b__) |
| { |
| struct grub_net_buff *a_ = *(struct grub_net_buff **) a__; |
| struct grub_net_buff *b_ = *(struct grub_net_buff **) b__; |
| struct tftphdr *a = (struct tftphdr *) a_->data; |
| struct tftphdr *b = (struct tftphdr *) b_->data; |
| /* We want the first elements to be on top. */ |
| return -cmp_block (grub_be_to_cpu16 (a->u.data.block), grub_be_to_cpu16 (b->u.data.block)); |
| } |
| |
| static grub_err_t |
| ack (tftp_data_t data, grub_uint64_t block) |
| { |
| struct tftphdr *tftph_ack; |
| grub_uint8_t nbdata[512]; |
| struct grub_net_buff nb_ack; |
| grub_err_t err; |
| |
| nb_ack.head = nbdata; |
| nb_ack.end = nbdata + sizeof (nbdata); |
| grub_netbuff_clear (&nb_ack); |
| grub_netbuff_reserve (&nb_ack, 512); |
| err = grub_netbuff_push (&nb_ack, sizeof (tftph_ack->opcode) |
| + sizeof (tftph_ack->u.ack.block)); |
| if (err) |
| return err; |
| |
| tftph_ack = (struct tftphdr *) nb_ack.data; |
| tftph_ack->opcode = grub_cpu_to_be16_compile_time (TFTP_ACK); |
| tftph_ack->u.ack.block = grub_cpu_to_be16 (block); |
| |
| err = grub_net_send_udp_packet (data->sock, &nb_ack); |
| if (err) |
| return err; |
| data->ack_sent = block; |
| return GRUB_ERR_NONE; |
| } |
| |
| static grub_err_t |
| tftp_receive (grub_net_udp_socket_t sock __attribute__ ((unused)), |
| struct grub_net_buff *nb, |
| void *f) |
| { |
| grub_file_t file = f; |
| struct tftphdr *tftph = (void *) nb->data; |
| tftp_data_t data = file->data; |
| grub_err_t err; |
| grub_uint8_t *ptr; |
| |
| if (nb->tail - nb->data < (grub_ssize_t) sizeof (tftph->opcode)) |
| { |
| grub_dprintf ("tftp", "TFTP packet too small\n"); |
| return GRUB_ERR_NONE; |
| } |
| |
| tftph = (struct tftphdr *) nb->data; |
| switch (grub_be_to_cpu16 (tftph->opcode)) |
| { |
| case TFTP_OACK: |
| data->block_size = TFTP_DEFAULTSIZE_PACKET; |
| data->have_oack = 1; |
| for (ptr = nb->data + sizeof (tftph->opcode); ptr < nb->tail;) |
| { |
| if (grub_memcmp (ptr, "tsize\0", sizeof ("tsize\0") - 1) == 0) |
| data->file_size = grub_strtoul ((char *) ptr + sizeof ("tsize\0") |
| - 1, 0, 0); |
| if (grub_memcmp (ptr, "blksize\0", sizeof ("blksize\0") - 1) == 0) |
| data->block_size = grub_strtoul ((char *) ptr + sizeof ("blksize\0") |
| - 1, 0, 0); |
| while (ptr < nb->tail && *ptr) |
| ptr++; |
| ptr++; |
| } |
| data->block = 0; |
| grub_netbuff_free (nb); |
| err = ack (data, 0); |
| grub_error_save (&data->save_err); |
| return GRUB_ERR_NONE; |
| case TFTP_DATA: |
| if (nb->tail - nb->data < (grub_ssize_t) (sizeof (tftph->opcode) |
| + sizeof (tftph->u.data.block))) |
| { |
| grub_dprintf ("tftp", "TFTP packet too small\n"); |
| return GRUB_ERR_NONE; |
| } |
| |
| err = grub_priority_queue_push (data->pq, &nb); |
| if (err) |
| return err; |
| |
| { |
| struct grub_net_buff **nb_top_p, *nb_top; |
| while (1) |
| { |
| nb_top_p = grub_priority_queue_top (data->pq); |
| if (!nb_top_p) |
| return GRUB_ERR_NONE; |
| nb_top = *nb_top_p; |
| tftph = (struct tftphdr *) nb_top->data; |
| if (cmp_block (grub_be_to_cpu16 (tftph->u.data.block), data->block + 1) >= 0) |
| break; |
| ack (data, grub_be_to_cpu16 (tftph->u.data.block)); |
| grub_netbuff_free (nb_top); |
| grub_priority_queue_pop (data->pq); |
| } |
| while (cmp_block (grub_be_to_cpu16 (tftph->u.data.block), data->block + 1) == 0) |
| { |
| unsigned size; |
| |
| grub_priority_queue_pop (data->pq); |
| |
| if (file->device->net->packs.count < 50) |
| err = ack (data, data->block + 1); |
| else |
| { |
| file->device->net->stall = 1; |
| err = 0; |
| } |
| if (err) |
| return err; |
| |
| err = grub_netbuff_pull (nb_top, sizeof (tftph->opcode) + |
| sizeof (tftph->u.data.block)); |
| if (err) |
| return err; |
| size = nb_top->tail - nb_top->data; |
| |
| data->block++; |
| if (size < data->block_size) |
| { |
| if (data->ack_sent < data->block) |
| ack (data, data->block); |
| file->device->net->eof = 1; |
| file->device->net->stall = 1; |
| grub_net_udp_close (data->sock); |
| data->sock = NULL; |
| } |
| /* Prevent garbage in broken cards. Is it still necessary |
| given that IP implementation has been fixed? |
| */ |
| if (size > data->block_size) |
| { |
| err = grub_netbuff_unput (nb_top, size - data->block_size); |
| if (err) |
| return err; |
| } |
| /* If there is data, puts packet in socket list. */ |
| if ((nb_top->tail - nb_top->data) > 0) |
| grub_net_put_packet (&file->device->net->packs, nb_top); |
| else |
| grub_netbuff_free (nb_top); |
| } |
| } |
| return GRUB_ERR_NONE; |
| case TFTP_ERROR: |
| data->have_oack = 1; |
| grub_netbuff_free (nb); |
| grub_error (GRUB_ERR_IO, (char *) tftph->u.err.errmsg); |
| grub_error_save (&data->save_err); |
| return GRUB_ERR_NONE; |
| default: |
| grub_netbuff_free (nb); |
| return GRUB_ERR_NONE; |
| } |
| } |
| |
| static void |
| destroy_pq (tftp_data_t data) |
| { |
| struct grub_net_buff **nb_p; |
| while ((nb_p = grub_priority_queue_top (data->pq))) |
| { |
| grub_netbuff_free (*nb_p); |
| grub_priority_queue_pop (data->pq); |
| } |
| |
| grub_priority_queue_destroy (data->pq); |
| } |
| |
| static grub_err_t |
| tftp_open (struct grub_file *file, const char *filename) |
| { |
| struct tftphdr *tftph; |
| char *rrq; |
| int i; |
| int rrqlen; |
| int hdrlen; |
| grub_uint8_t open_data[1500]; |
| struct grub_net_buff nb; |
| tftp_data_t data; |
| grub_err_t err; |
| grub_uint8_t *nbd; |
| grub_net_network_level_address_t addr; |
| |
| data = grub_zalloc (sizeof (*data)); |
| if (!data) |
| return grub_errno; |
| |
| nb.head = open_data; |
| nb.end = open_data + sizeof (open_data); |
| grub_netbuff_clear (&nb); |
| |
| grub_netbuff_reserve (&nb, 1500); |
| err = grub_netbuff_push (&nb, sizeof (*tftph)); |
| if (err) |
| { |
| grub_free (data); |
| return err; |
| } |
| |
| tftph = (struct tftphdr *) nb.data; |
| |
| rrq = (char *) tftph->u.rrq; |
| rrqlen = 0; |
| |
| tftph->opcode = grub_cpu_to_be16_compile_time (TFTP_RRQ); |
| grub_strcpy (rrq, filename); |
| rrqlen += grub_strlen (filename) + 1; |
| rrq += grub_strlen (filename) + 1; |
| |
| grub_strcpy (rrq, "octet"); |
| rrqlen += grub_strlen ("octet") + 1; |
| rrq += grub_strlen ("octet") + 1; |
| |
| grub_strcpy (rrq, "blksize"); |
| rrqlen += grub_strlen ("blksize") + 1; |
| rrq += grub_strlen ("blksize") + 1; |
| |
| grub_strcpy (rrq, "1024"); |
| rrqlen += grub_strlen ("1024") + 1; |
| rrq += grub_strlen ("1024") + 1; |
| |
| grub_strcpy (rrq, "tsize"); |
| rrqlen += grub_strlen ("tsize") + 1; |
| rrq += grub_strlen ("tsize") + 1; |
| |
| grub_strcpy (rrq, "0"); |
| rrqlen += grub_strlen ("0") + 1; |
| rrq += grub_strlen ("0") + 1; |
| hdrlen = sizeof (tftph->opcode) + rrqlen; |
| |
| err = grub_netbuff_unput (&nb, nb.tail - (nb.data + hdrlen)); |
| if (err) |
| { |
| grub_free (data); |
| return err; |
| } |
| |
| file->not_easily_seekable = 1; |
| file->data = data; |
| |
| data->pq = grub_priority_queue_new (sizeof (struct grub_net_buff *), cmp); |
| if (!data->pq) |
| { |
| grub_free (data); |
| return grub_errno; |
| } |
| |
| err = grub_net_resolve_address (file->device->net->server, &addr); |
| if (err) |
| { |
| destroy_pq (data); |
| grub_free (data); |
| return err; |
| } |
| |
| data->sock = grub_net_udp_open (addr, |
| TFTP_SERVER_PORT, tftp_receive, |
| file); |
| if (!data->sock) |
| { |
| destroy_pq (data); |
| grub_free (data); |
| return grub_errno; |
| } |
| |
| /* Receive OACK packet. */ |
| nbd = nb.data; |
| for (i = 0; i < GRUB_NET_TRIES; i++) |
| { |
| nb.data = nbd; |
| err = grub_net_send_udp_packet (data->sock, &nb); |
| if (err) |
| { |
| grub_net_udp_close (data->sock); |
| destroy_pq (data); |
| grub_free (data); |
| return err; |
| } |
| grub_net_poll_cards (GRUB_NET_INTERVAL + (i * GRUB_NET_INTERVAL_ADDITION), |
| &data->have_oack); |
| if (data->have_oack) |
| break; |
| } |
| |
| if (!data->have_oack) |
| grub_error (GRUB_ERR_TIMEOUT, N_("time out opening `%s'"), filename); |
| else |
| grub_error_load (&data->save_err); |
| if (grub_errno) |
| { |
| grub_net_udp_close (data->sock); |
| destroy_pq (data); |
| grub_free (data); |
| return grub_errno; |
| } |
| |
| file->size = data->file_size; |
| |
| return GRUB_ERR_NONE; |
| } |
| |
| static grub_err_t |
| tftp_close (struct grub_file *file) |
| { |
| tftp_data_t data = file->data; |
| |
| if (data->sock) |
| { |
| grub_uint8_t nbdata[512]; |
| grub_err_t err; |
| struct grub_net_buff nb_err; |
| struct tftphdr *tftph; |
| |
| nb_err.head = nbdata; |
| nb_err.end = nbdata + sizeof (nbdata); |
| |
| grub_netbuff_clear (&nb_err); |
| grub_netbuff_reserve (&nb_err, 512); |
| err = grub_netbuff_push (&nb_err, sizeof (tftph->opcode) |
| + sizeof (tftph->u.err.errcode) |
| + sizeof ("closed")); |
| if (!err) |
| { |
| tftph = (struct tftphdr *) nb_err.data; |
| tftph->opcode = grub_cpu_to_be16_compile_time (TFTP_ERROR); |
| tftph->u.err.errcode = grub_cpu_to_be16_compile_time (TFTP_EUNDEF); |
| grub_memcpy (tftph->u.err.errmsg, "closed", sizeof ("closed")); |
| |
| err = grub_net_send_udp_packet (data->sock, &nb_err); |
| } |
| if (err) |
| grub_print_error (); |
| grub_net_udp_close (data->sock); |
| } |
| destroy_pq (data); |
| grub_free (data); |
| return GRUB_ERR_NONE; |
| } |
| |
| static grub_err_t |
| tftp_packets_pulled (struct grub_file *file) |
| { |
| tftp_data_t data = file->data; |
| if (file->device->net->packs.count >= 50) |
| return 0; |
| |
| if (!file->device->net->eof) |
| file->device->net->stall = 0; |
| if (data->ack_sent >= data->block) |
| return 0; |
| return ack (data, data->block); |
| } |
| |
| static struct grub_net_app_protocol grub_tftp_protocol = |
| { |
| .name = "tftp", |
| .open = tftp_open, |
| .close = tftp_close, |
| .packets_pulled = tftp_packets_pulled |
| }; |
| |
| GRUB_MOD_INIT (tftp) |
| { |
| grub_net_app_level_register (&grub_tftp_protocol); |
| } |
| |
| GRUB_MOD_FINI (tftp) |
| { |
| grub_net_app_level_unregister (&grub_tftp_protocol); |
| } |