| #include <minmax.h> |
| #include <net.h> |
| #include "pxe.h" |
| #include "url.h" |
| #include "tftp.h" |
| |
| const uint8_t TimeoutTable[] = { |
| 2, 2, 3, 3, 4, 5, 6, 7, 9, 10, 12, 15, 18, 21, 26, 31, 37, 44, |
| 53, 64, 77, 92, 110, 132, 159, 191, 229, 255, 255, 255, 255, 0 |
| }; |
| struct tftp_packet { |
| uint16_t opcode; |
| uint16_t serial; |
| char data[]; |
| }; |
| |
| static void tftp_error(struct inode *file, uint16_t errnum, |
| const char *errstr); |
| |
| static void tftp_close_file(struct inode *inode) |
| { |
| struct pxe_pvt_inode *socket = PVT(inode); |
| if (!socket->tftp_goteof) { |
| tftp_error(inode, 0, "No error, file close"); |
| } |
| core_udp_close(socket); |
| } |
| |
| /** |
| * Send an ERROR packet. This is used to terminate a connection. |
| * |
| * @inode: Inode structure |
| * @errnum: Error number (network byte order) |
| * @errstr: Error string (included in packet) |
| */ |
| static void tftp_error(struct inode *inode, uint16_t errnum, |
| const char *errstr) |
| { |
| static struct { |
| uint16_t err_op; |
| uint16_t err_num; |
| char err_msg[64]; |
| } __packed err_buf; |
| int len = min(strlen(errstr), sizeof(err_buf.err_msg)-1); |
| struct pxe_pvt_inode *socket = PVT(inode); |
| |
| err_buf.err_op = TFTP_ERROR; |
| err_buf.err_num = errnum; |
| memcpy(err_buf.err_msg, errstr, len); |
| err_buf.err_msg[len] = '\0'; |
| |
| core_udp_send(socket, &err_buf, 4 + len + 1); |
| } |
| |
| /** |
| * Send ACK packet. This is a common operation and so is worth canning. |
| * |
| * @param: inode, Inode pointer |
| * @param: ack_num, Packet # to ack (host byte order) |
| * |
| */ |
| static void ack_packet(struct inode *inode, uint16_t ack_num) |
| { |
| static uint16_t ack_packet_buf[2]; |
| struct pxe_pvt_inode *socket = PVT(inode); |
| |
| /* Packet number to ack */ |
| ack_packet_buf[0] = TFTP_ACK; |
| ack_packet_buf[1] = htons(ack_num); |
| |
| core_udp_send(socket, ack_packet_buf, 4); |
| } |
| |
| /* |
| * Get a fresh packet if the buffer is drained, and we haven't hit |
| * EOF yet. The buffer should be filled immediately after draining! |
| */ |
| static void tftp_get_packet(struct inode *inode) |
| { |
| uint16_t last_pkt; |
| const uint8_t *timeout_ptr; |
| uint8_t timeout; |
| uint16_t buffersize; |
| uint16_t serial; |
| jiffies_t oldtime; |
| struct tftp_packet *pkt = NULL; |
| uint16_t buf_len; |
| struct pxe_pvt_inode *socket = PVT(inode); |
| uint16_t src_port; |
| uint32_t src_ip; |
| int err; |
| |
| /* |
| * Start by ACKing the previous packet; this should cause |
| * the next packet to be sent. |
| */ |
| timeout_ptr = TimeoutTable; |
| timeout = *timeout_ptr++; |
| oldtime = jiffies(); |
| |
| ack_again: |
| ack_packet(inode, socket->tftp_lastpkt); |
| |
| while (timeout) { |
| buf_len = socket->tftp_blksize + 4; |
| err = core_udp_recv(socket, socket->tftp_pktbuf, &buf_len, |
| &src_ip, &src_port); |
| if (err) { |
| jiffies_t now = jiffies(); |
| |
| if (now-oldtime >= timeout) { |
| oldtime = now; |
| timeout = *timeout_ptr++; |
| if (!timeout) |
| break; |
| goto ack_again; |
| } |
| continue; |
| } |
| |
| if (buf_len < 4) /* Bad size for a DATA packet */ |
| continue; |
| |
| pkt = (struct tftp_packet *)(socket->tftp_pktbuf); |
| if (pkt->opcode != TFTP_DATA) /* Not a data packet */ |
| continue; |
| |
| /* If goes here, recevie OK, break */ |
| break; |
| } |
| |
| /* time runs out */ |
| if (timeout == 0) |
| kaboom(); |
| |
| last_pkt = socket->tftp_lastpkt; |
| last_pkt++; |
| serial = ntohs(pkt->serial); |
| if (serial != last_pkt) { |
| /* |
| * Wrong packet, ACK the packet and try again. |
| * This is presumably because the ACK got lost, |
| * so the server just resent the previous packet. |
| */ |
| #if 0 |
| printf("Wrong packet, wanted %04x, got %04x\n", \ |
| htons(last_pkt), htons(*(uint16_t *)(data+2))); |
| #endif |
| goto ack_again; |
| } |
| |
| /* It's the packet we want. We're also EOF if the size < blocksize */ |
| socket->tftp_lastpkt = last_pkt; /* Update last packet number */ |
| buffersize = buf_len - 4; /* Skip TFTP header */ |
| socket->tftp_dataptr = socket->tftp_pktbuf + 4; |
| socket->tftp_filepos += buffersize; |
| socket->tftp_bytesleft = buffersize; |
| if (buffersize < socket->tftp_blksize) { |
| /* it's the last block, ACK packet immediately */ |
| ack_packet(inode, serial); |
| |
| /* Make sure we know we are at end of file */ |
| inode->size = socket->tftp_filepos; |
| socket->tftp_goteof = 1; |
| tftp_close_file(inode); |
| } |
| } |
| |
| const struct pxe_conn_ops tftp_conn_ops = { |
| .fill_buffer = tftp_get_packet, |
| .close = tftp_close_file, |
| }; |
| |
| /** |
| * Open a TFTP connection to the server |
| * |
| * @param:inode, the inode to store our state in |
| * @param:ip, the ip to contact to get the file |
| * @param:filename, the file we wanna open |
| * |
| * @out: open_file_t structure, stores in file->open_file |
| * @out: the lenght of this file, stores in file->file_len |
| * |
| */ |
| void tftp_open(struct url_info *url, int flags, struct inode *inode, |
| const char **redir) |
| { |
| struct pxe_pvt_inode *socket = PVT(inode); |
| char *buf; |
| uint16_t buf_len; |
| char *p; |
| char *options; |
| char *data; |
| static const char rrq_tail[] = "octet\0""tsize\0""0\0""blksize\0""1408"; |
| char rrq_packet_buf[2+2*FILENAME_MAX+sizeof rrq_tail]; |
| char reply_packet_buf[PKTBUF_SIZE]; |
| int err; |
| int buffersize; |
| int rrq_len; |
| const uint8_t *timeout_ptr; |
| jiffies_t timeout; |
| jiffies_t oldtime; |
| uint16_t opcode; |
| uint16_t blk_num; |
| uint64_t opdata; |
| uint16_t src_port; |
| uint32_t src_ip; |
| |
| (void)redir; /* TFTP does not redirect */ |
| (void)flags; |
| |
| if (url->type != URL_OLD_TFTP) { |
| /* |
| * The TFTP URL specification allows the TFTP to end with a |
| * ;mode= which we just ignore. |
| */ |
| url_unescape(url->path, ';'); |
| } |
| |
| if (!url->port) |
| url->port = TFTP_PORT; |
| |
| socket->ops = &tftp_conn_ops; |
| if (core_udp_open(socket)) |
| return; |
| |
| buf = rrq_packet_buf; |
| *(uint16_t *)buf = TFTP_RRQ; /* TFTP opcode */ |
| buf += 2; |
| |
| buf = stpcpy(buf, url->path); |
| |
| buf++; /* Point *past* the final NULL */ |
| memcpy(buf, rrq_tail, sizeof rrq_tail); |
| buf += sizeof rrq_tail; |
| |
| rrq_len = buf - rrq_packet_buf; |
| |
| timeout_ptr = TimeoutTable; /* Reset timeout */ |
| sendreq: |
| timeout = *timeout_ptr++; |
| if (!timeout) |
| return; /* No file available... */ |
| oldtime = jiffies(); |
| |
| core_udp_sendto(socket, rrq_packet_buf, rrq_len, url->ip, url->port); |
| |
| /* If the WRITE call fails, we let the timeout take care of it... */ |
| wait_pkt: |
| for (;;) { |
| buf_len = sizeof(reply_packet_buf); |
| |
| err = core_udp_recv(socket, reply_packet_buf, &buf_len, |
| &src_ip, &src_port); |
| if (err) { |
| jiffies_t now = jiffies(); |
| if (now - oldtime >= timeout) |
| goto sendreq; |
| } else { |
| /* Make sure the packet actually came from the server and |
| is long enough for a TFTP opcode */ |
| dprintf("tftp_open: got packet buflen=%d\n", buf_len); |
| if ((src_ip == url->ip) && (buf_len >= 2)) |
| break; |
| } |
| } |
| |
| core_udp_disconnect(socket); |
| core_udp_connect(socket, src_ip, src_port); |
| |
| /* filesize <- -1 == unknown */ |
| inode->size = -1; |
| socket->tftp_blksize = TFTP_BLOCKSIZE; |
| buffersize = buf_len - 2; /* bytes after opcode */ |
| |
| /* |
| * Get the opcode type, and parse it |
| */ |
| opcode = *(uint16_t *)reply_packet_buf; |
| switch (opcode) { |
| case TFTP_ERROR: |
| inode->size = 0; |
| goto done; /* ERROR reply; don't try again */ |
| |
| case TFTP_DATA: |
| /* |
| * If the server doesn't support any options, we'll get a |
| * DATA reply instead of OACK. Stash the data in the file |
| * buffer and go with the default value for all options... |
| * |
| * We got a DATA packet, meaning no options are |
| * suported. Save the data away and consider the |
| * length undefined, *unless* this is the only |
| * data packet... |
| */ |
| buffersize -= 2; |
| if (buffersize < 0) |
| goto wait_pkt; |
| data = reply_packet_buf + 2; |
| blk_num = ntohs(*(uint16_t *)data); |
| data += 2; |
| if (blk_num != 1) |
| goto wait_pkt; |
| socket->tftp_lastpkt = blk_num; |
| if (buffersize > TFTP_BLOCKSIZE) |
| goto err_reply; /* Corrupt */ |
| |
| socket->tftp_pktbuf = malloc(TFTP_BLOCKSIZE + 4); |
| if (!socket->tftp_pktbuf) |
| goto err_reply; /* Internal error */ |
| |
| if (buffersize < TFTP_BLOCKSIZE) { |
| /* |
| * This is the final EOF packet, already... |
| * We know the filesize, but we also want to |
| * ack the packet and set the EOF flag. |
| */ |
| inode->size = buffersize; |
| socket->tftp_goteof = 1; |
| ack_packet(inode, blk_num); |
| } |
| |
| socket->tftp_bytesleft = buffersize; |
| socket->tftp_dataptr = socket->tftp_pktbuf; |
| memcpy(socket->tftp_pktbuf, data, buffersize); |
| goto done; |
| |
| case TFTP_OACK: |
| /* |
| * Now we need to parse the OACK packet to get the transfer |
| * and packet sizes. |
| */ |
| |
| options = reply_packet_buf + 2; |
| p = options; |
| |
| while (buffersize) { |
| const char *opt = p; |
| |
| /* |
| * If we find an option which starts with a NUL byte, |
| * (a null option), we're either seeing garbage that some |
| * TFTP servers add to the end of the packet, or we have |
| * no clue how to parse the rest of the packet (what is |
| * an option name and what is a value?) In either case, |
| * discard the rest. |
| */ |
| if (!*opt) |
| goto done; |
| |
| while (buffersize) { |
| if (!*p) |
| break; /* Found a final null */ |
| *p++ |= 0x20; |
| buffersize--; |
| } |
| if (!buffersize) |
| break; /* Unterminated option */ |
| |
| /* Consume the terminal null */ |
| p++; |
| buffersize--; |
| |
| if (!buffersize) |
| break; /* No option data */ |
| |
| opdata = 0; |
| |
| /* do convert a number-string to decimal number, just like atoi */ |
| while (buffersize--) { |
| uint8_t d = *p++; |
| if (d == '\0') |
| break; /* found a final null */ |
| d -= '0'; |
| if (d > 9) |
| goto err_reply; /* Not a decimal digit */ |
| opdata = opdata*10 + d; |
| } |
| |
| if (!strcmp(opt, "tsize")) |
| inode->size = opdata; |
| else if (!strcmp(opt, "blksize")) |
| socket->tftp_blksize = opdata; |
| else |
| goto err_reply; /* Non-negotitated option returned, |
| no idea what it means ...*/ |
| |
| |
| } |
| |
| if (socket->tftp_blksize < 64 || socket->tftp_blksize > PKTBUF_SIZE) |
| goto err_reply; |
| |
| /* Parsing successful, allocate buffer */ |
| socket->tftp_pktbuf = malloc(socket->tftp_blksize + 4); |
| if (!socket->tftp_pktbuf) |
| goto err_reply; |
| else |
| goto done; |
| |
| default: |
| printf("TFTP unknown opcode %d\n", ntohs(opcode)); |
| goto err_reply; |
| } |
| |
| err_reply: |
| /* Build the TFTP error packet */ |
| tftp_error(inode, TFTP_EOPTNEG, "TFTP protocol error"); |
| inode->size = 0; |
| |
| done: |
| if (!inode->size) |
| core_udp_close(socket); |
| |
| return; |
| } |