aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/t2hproxy
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/t2hproxy')
-rw-r--r--contrib/t2hproxy/README95
-rw-r--r--contrib/t2hproxy/T2hproxy.java508
-rw-r--r--contrib/t2hproxy/build.xml19
-rwxr-xr-xcontrib/t2hproxy/runT2proxy.sh15
-rwxr-xr-xcontrib/t2hproxy/t2hproxy.pl174
-rw-r--r--contrib/t2hproxy/t2hproxy.xinetd29
6 files changed, 840 insertions, 0 deletions
diff --git a/contrib/t2hproxy/README b/contrib/t2hproxy/README
new file mode 100644
index 000000000..ce5e1e9ba
--- /dev/null
+++ b/contrib/t2hproxy/README
@@ -0,0 +1,95 @@
+T2hproxy
+
+This is a TFTP to HTTP proxy. To the TFTP client it looks like a TFTP
+server. To the HTTP server it looks like a HTTP client. So you can store
+your boot files on the HTTP server. Or even create them with a CGI
+program. E.g. if you can get dhcpd to send a filename which has strings
+representing attributes of the client, as determined from the DHCP
+request, then you can get the CGI program to parse this and send the
+appropriate image, which might even be synthesised.
+
+There are two versions of the proxy, in Perl and in Java.
+
+1. The Perl version.
+
+This is the original quick Perl hack conceived in a moment of madness.
+:-) Perl is great for prototyping.
+
+To run it, you need Perl 5.8.0 or later and all the Perl modules listed
+at the top of the program installed. Edit and install the xinetd config
+file as /etc/xinetd.d/t2hproxy and restart xinetd. The prefix is the
+string that is prepended to all filenames to form the URL requested from
+the HTTP server. Remember you need the trailing / if the filenames don't
+start with /.
+
+This is only a proof-of concept. It has these drawbacks at the moment:
+
++ (I don't consider this a draback, but some may.) It's started from
+xinetd because xinetd handles all the socket listening, IP address
+checking, rate limiting, etc.
+
++ It has no cache. Use a proxy to do the caching (there's a --proxy
+option). This also takes care of fetching from outside a firewall.
+
++ It reads the entire HTTP content into memory before serving. Ideally
+it should stream it from the HTTP server to minimise memory usage. This
+is a serious drawback for booting lots of clients. Each instance of the
+server will consume an amount of memory equal to the size of image
+loaded.
+
++ If the HTTP server is at the end of a slow link there is a delay
+before the first data block is sent. The client may timeout before
+then. Another reason for streaming, as this allows the first block to
+be sent sooner. A local cache primed with the images in advance may
+help. Using the blocksize option helps here because this causes the
+server to send the OACK to the client immediately before the data is
+fetched and this prevents it from starting up another connection.
+
++ The transfer size may not be obtainable from the HTTP headers in all
+cases, e.g. a CGI constructed image. This matters for clients that need
+the tsize extension, which is not supported at the moment.
+
+If I'm feeling masochistic I may write a Java version, which should take
+care of the multi-threading and streaming.
+
+2. The Java version
+
+The main problem with the Perl version is that it does not stream the
+HTTP input but sucks it all in at once. As mentioned, this causes a
+delay as well as requiring memory to hold the image. I could fix this by
+doing the polling on the HTTP socket myself instead of letting LWP do
+it, but that's for later. Java has streaming facilities as well as
+threading and is also somewhat portable. So I decided to be masochistic
+and give it a go. But boy is Java bureaucratic.
+
+You will need a Java 1.4 JRE, because I use the java.nio classes; and
+the commons-httpclient and commons-logging jars from the
+jakarta.apache.org project. As I understand it, there are several ways
+to get those jars on your classpath. One is to put it in the directory
+where your java extensions jars are kept, normally
+$JAVA_HOME/jre/lib/ext. But it may not be writable to you. Another is to
+set your $CLASSPATH variable to have those jars in the path. A third is
+to use the -cp option of the java interpreter, see the shell script
+runT2hproxy for details.
+
+All the source is in one Java file. build.xml is a "Makefile" for ant to
+compile and jar it. You should then edit runT2proxy.sh as required, then
+start it. As with the Perl version, the prefix is what's prepended to
+the filenames requested by the TFTP client, and the proxy is the
+host:port string for the proxy if you are using one. On *ix you will
+need root permission to listen on ports below 1024 (TFTP is at 69 UDP by
+default).
+
+Currently it logs to stderr, but you can change this by downloading and
+installing the log4j jar from jakarta.apache.org and instructing
+commons-logging to use that, with a command line property setting and a
+property file. Destinations could be syslog, or a file, or an event
+logger, or...; it's supposedly very flexible.
+
+3. Licensing
+
+All this code is GPLed. For details read the file COPYING found in the
+Etherboot top directory since it currently bundled with Etherboot. I
+don't see the point of including COPYING in every directory.
+
+Ken Yap, October 2003
diff --git a/contrib/t2hproxy/T2hproxy.java b/contrib/t2hproxy/T2hproxy.java
new file mode 100644
index 000000000..cfe1d1a79
--- /dev/null
+++ b/contrib/t2hproxy/T2hproxy.java
@@ -0,0 +1,508 @@
+/*
+ * TFTP to HTTP proxy in Java
+ *
+ * Copyright Ken Yap 2003
+ * Released under GPL2
+ */
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.FileInputStream;
+import java.io.BufferedInputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.String;
+import java.lang.StringBuffer;
+import java.lang.Thread;
+import java.lang.NumberFormatException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.BufferUnderflowException;
+import java.util.HashMap;
+import java.util.Properties;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HostConfiguration;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.methods.GetMethod;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Description of the Class
+ *
+ *@author ken
+ *@created 24 September 2003
+ */
+public class T2hproxy implements Runnable {
+ /**
+ * Description of the Field
+ */
+ public final static String NAME = T2hproxy.class.getName();
+ /**
+ * Description of the Field
+ */
+ public final static String VERSION = "0.1";
+ /**
+ * Description of the Field
+ */
+ public final static int MTU = 1500;
+ /**
+ * Description of the Field
+ */
+ public final static short TFTP_RRQ = 1;
+ /**
+ * Description of the Field
+ */
+ public final static short TFTP_DATA = 3;
+ /**
+ * Description of the Field
+ */
+ public final static short TFTP_ACK = 4;
+ /**
+ * Description of the Field
+ */
+ public final static short TFTP_ERROR = 5;
+ /**
+ * Description of the Field
+ */
+ public final static short TFTP_OACK = 6;
+ /**
+ * Description of the Field
+ */
+ public final static short ERR_NOFILE = 1;
+ /**
+ * Description of the Field
+ */
+ public final static short ERR_ILLOP = 4;
+ /**
+ * Description of the Field
+ */
+ public final static int MAX_RETRIES = 5;
+ /**
+ * TFTP timeout in milliseconds
+ */
+ public final static int TFTP_ACK_TIMEOUT = 2000;
+ /**
+ * Description of the Field
+ */
+ public final static int DEFAULT_PROXY_PORT = 3128;
+
+ private static Log log = LogFactory.getLog(T2hproxy.class);
+ /**
+ * The members below must be per thread and must not share any storage with
+ * the main thread
+ */
+ private DatagramSocket responsesocket;
+ private DatagramPacket response;
+ private InetAddress iaddr;
+ private int port;
+ private byte[] req;
+ private String prefix;
+ private String proxy = null;
+ private int timeout;
+ private HashMap options = new HashMap();
+ private int blocksize = 512;
+ private HttpClient client = new HttpClient();
+ private HttpMethod method;
+ private BufferedInputStream bstream = null;
+ private String message;
+
+
+ /**
+ * Constructor for the T2hproxy object
+ *
+ *@param i Description of the Parameter
+ *@param p Description of the Parameter
+ *@param b Description of the Parameter
+ *@param pf Description of the Parameter
+ *@param pr Description of the Parameter
+ *@param t Timeout for HTTP GET
+ */
+ public T2hproxy(InetAddress i, int p, byte[] b, String pf, String pr, int t) {
+ iaddr = i;
+ port = p;
+ // make a copy of the request buffer
+ req = new byte[b.length];
+ System.arraycopy(b, 0, req, 0, b.length);
+ prefix = pf;
+ // proxy can be null
+ proxy = pr;
+ timeout = t;
+ }
+
+
+ /**
+ * Extract an asciz string from bufer
+ *
+ *@param buffer Description of the Parameter
+ *@return The asciz value
+ */
+ private String getAsciz(ByteBuffer buffer) {
+ StringBuffer s = new StringBuffer();
+ try {
+ byte b;
+ while ((b = buffer.get()) != 0) {
+ s.append((char) b);
+ }
+ } catch (BufferUnderflowException e) {
+ } finally {
+ return (s.toString());
+ }
+ }
+
+
+ /**
+ * Convert a string of digits to a number, invalid => 0
+ *
+ *@param s Description of the Parameter
+ *@return Description of the Return Value
+ */
+ private int atoi(String s) {
+ if (s == null) {
+ return (0);
+ }
+ int value = 0;
+ try {
+ value = (new Integer(s)).intValue();
+ } catch (NumberFormatException e) {
+ }
+ return (value);
+ }
+
+
+ /**
+ * Wait for ack packet with timeout
+ *
+ *@return Return block number acked
+ */
+ private int waitForAck() {
+ DatagramPacket ack = new DatagramPacket(new byte[MTU], MTU);
+ try {
+ do {
+ responsesocket.setSoTimeout(TFTP_ACK_TIMEOUT);
+ responsesocket.receive(ack);
+ } while (!ack.getAddress().equals(iaddr) || ack.getPort() != port);
+ } catch (SocketTimeoutException e) {
+ return (-1);
+ } catch (Exception e) {
+ log.info(e.toString(), e);
+ }
+ ByteBuffer buffer = ByteBuffer.wrap(ack.getData(), ack.getOffset(), ack.getLength() - ack.getOffset());
+ short op;
+ if ((op = buffer.getShort()) == TFTP_ACK) {
+ return ((int) buffer.getShort());
+ } else if (op == TFTP_ERROR) {
+ return (-2);
+ }
+ return (-3);
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@param error Description of the Parameter
+ *@param message Description of the Parameter
+ */
+ private void sendError(short error, String message) {
+ ByteBuffer buffer = ByteBuffer.wrap(response.getData());
+ buffer.putShort(TFTP_ERROR).putShort(error).put(message.getBytes());
+ response.setLength(buffer.position());
+ try {
+ responsesocket.send(response);
+ } catch (Exception e) {
+ log.info(e.toString(), e);
+ }
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@return Description of the Return Value
+ */
+ private boolean sendOackRecvAck() {
+ ByteBuffer buffer = ByteBuffer.wrap(response.getData());
+ buffer.putShort(TFTP_OACK).put("blksize".getBytes()).put((byte) 0).put(String.valueOf(blocksize).getBytes()).put((byte) 0);
+ response.setLength(buffer.position());
+ int retry;
+ for (retry = 0; retry < MAX_RETRIES; retry++) {
+ try {
+ responsesocket.send(response);
+ } catch (Exception e) {
+ log.info(e.toString(), e);
+ }
+ if (waitForAck() == 0) {
+ log.debug("Ack received");
+ break;
+ }
+ }
+ return (retry < MAX_RETRIES);
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@param block Description of the Parameter
+ *@return Description of the Return Value
+ */
+ private boolean sendDataBlock(int block) {
+ int retry;
+ for (retry = 0; retry < MAX_RETRIES; retry++) {
+ try {
+ responsesocket.send(response);
+ } catch (Exception e) {
+ log.info(e.toString(), e);
+ }
+ int ablock;
+ if ((ablock = waitForAck()) == block) {
+ log.debug("Ack received for " + ablock);
+ break;
+ } else if (ablock == -1) {
+ log.info("Timeout waiting for ack");
+ } else if (ablock == -2) {
+ return (false);
+ } else {
+ log.info("Unknown opcode from ack");
+ }
+ }
+ return (retry < MAX_RETRIES);
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@param buffer Description of the Parameter
+ *@return Description of the Return Value
+ */
+ private boolean handleOptions(ByteBuffer buffer) {
+ for (; ; ) {
+ String option = getAsciz(buffer);
+ String value = getAsciz(buffer);
+ if (option.equals("") || value.equals("")) {
+ break;
+ }
+ log.info(option + " " + value);
+ options.put(option, value);
+ }
+ blocksize = atoi((String) options.get("blksize"));
+ if (blocksize < 512) {
+ blocksize = 512;
+ }
+ if (blocksize > 1432) {
+ blocksize = 1432;
+ }
+ return (sendOackRecvAck());
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@param url Description of the Parameter
+ */
+ private void makeStream(String url) {
+ // establish a connection within timeout milliseconds
+ client.setConnectionTimeout(timeout);
+ if (proxy != null) {
+ String[] hostport = proxy.split(":");
+ int port = DEFAULT_PROXY_PORT;
+ if (hostport.length > 1) {
+ port = atoi(hostport[1]);
+ if (port == 0) {
+ port = DEFAULT_PROXY_PORT;
+ }
+ }
+ log.info("Proxy is " + hostport[0] + ":" + port);
+ client.getHostConfiguration().setProxy(hostport[0], port);
+ }
+ // create a method object
+ method = new GetMethod(url);
+ method.setFollowRedirects(true);
+ method.setStrictMode(false);
+ try {
+ int status;
+ if ((status = client.executeMethod(method)) != 200) {
+ log.info(message = method.getStatusText());
+ return;
+ }
+ bstream = new BufferedInputStream(method.getResponseBodyAsStream());
+ } catch (HttpException he) {
+ message = he.getMessage();
+ } catch (IOException ioe) {
+ message = "Unable to get " + url;
+ }
+ }
+
+
+ /**
+ * Reads a block of data from URL stream
+ *
+ *@param stream Description of the Parameter
+ *@param data Description of the Parameter
+ *@param blocksize Description of the Parameter
+ *@param offset Description of the Parameter
+ *@return Number of bytes read
+ */
+ private int readBlock(BufferedInputStream stream, byte[] data, int offset, int blocksize) {
+ int status;
+ int nread = 0;
+ while (nread < blocksize) {
+ try {
+ status = stream.read(data, offset + nread, blocksize - nread);
+ } catch (Exception e) {
+ return (-1);
+ }
+ if (status < 0) {
+ return (nread);
+ }
+ nread += status;
+ }
+ return (nread);
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@param filename Description of the Parameter
+ */
+ private void doRrq(String filename) {
+ String url = prefix + filename;
+ log.info("GET " + url);
+ makeStream(url);
+ if (bstream == null) {
+ log.info(message);
+ sendError(ERR_NOFILE, message);
+ return;
+ }
+ // read directly into send buffer to avoid buffer copying
+ byte[] data;
+ ByteBuffer buffer = ByteBuffer.wrap(data = response.getData());
+ // dummy puts to get start position of data
+ buffer.putShort(TFTP_DATA).putShort((short) 0);
+ int start = buffer.position();
+ int length;
+ int block = 1;
+ do {
+ length = readBlock(bstream, data, start, blocksize);
+ block &= 0xffff;
+ log.debug("Block " + block + " " + length);
+ // fill in the block number
+ buffer.position(0);
+ buffer.putShort(TFTP_DATA).putShort((short) block);
+ response.setLength(start + length);
+ if (!sendDataBlock(block)) {
+ break;
+ }
+ buffer.position(start);
+ block++;
+ } while (length >= blocksize);
+ log.info("Closing TFTP session");
+ // clean up the connection resources
+ method.releaseConnection();
+ method.recycle();
+ }
+
+
+ /**
+ * Main processing method for the T2hproxy object
+ */
+ public void run() {
+ ByteBuffer buffer = ByteBuffer.wrap(req);
+ buffer.getShort();
+ String filename = getAsciz(buffer);
+ String mode = getAsciz(buffer);
+ log.info(filename + " " + mode);
+ response = new DatagramPacket(new byte[MTU], MTU, iaddr, port);
+ try {
+ responsesocket = new DatagramSocket();
+ } catch (SocketException e) {
+ log.info(e.toString(), e);
+ return;
+ }
+ if (!handleOptions(buffer)) {
+ return;
+ }
+ doRrq(filename);
+ }
+
+
+ /**
+ * Description of the Method
+ *
+ *@param s Description of the Parameter
+ *@param r Description of the Parameter
+ *@param prefix Description of the Parameter
+ *@param proxy Description of the Parameter
+ *@param timeout Description of the Parameter
+ */
+ public static void handleRequest(DatagramSocket s, DatagramPacket r, String prefix, String proxy, int timeout) {
+ log.info("Connection from " + r.getAddress().getCanonicalHostName() + ":" + r.getPort());
+ ByteBuffer buffer = ByteBuffer.wrap(r.getData(), r.getOffset(), r.getLength() - r.getOffset());
+ if (buffer.getShort() != TFTP_RRQ) {
+ DatagramPacket error = new DatagramPacket(new byte[MTU], MTU);
+ ByteBuffer rbuf = ByteBuffer.wrap(error.getData());
+ rbuf.putShort(TFTP_ERROR).putShort(ERR_ILLOP).put("Illegal operation".getBytes());
+ error.setLength(rbuf.position());
+ try {
+ s.send(error);
+ } catch (Exception e) {
+ log.info(e.toString(), e);
+ }
+ return;
+ }
+ // fork thread
+ new Thread(new T2hproxy(r.getAddress(), r.getPort(), r.getData(), prefix, proxy, timeout)).start();
+ }
+
+
+ /**
+ * The main program for the T2hproxy class
+ *
+ *@param argv The command line arguments
+ *@exception IOException Description of the Exception
+ */
+ public static void main(String[] argv) throws IOException {
+ log.info(T2hproxy.NAME + "." + T2hproxy.VERSION);
+ int port = Integer.getInteger(T2hproxy.NAME + ".port", 69).intValue();
+ String prefix = System.getProperty(T2hproxy.NAME + ".prefix", "http://localhost/");
+ String proxy = System.getProperty(T2hproxy.NAME + ".proxy");
+ int timeout = Integer.getInteger(T2hproxy.NAME + ".timeout", 5000).intValue();
+ String propfile = System.getProperty(T2hproxy.NAME + ".properties");
+ if (propfile != null) {
+ FileInputStream pf = new FileInputStream(propfile);
+ Properties p = new Properties(System.getProperties());
+ p.load(pf);
+ // set the system properties
+ System.setProperties(p);
+ }
+ DatagramSocket requestsocket;
+ try {
+ requestsocket = new DatagramSocket(port);
+ } catch (SocketException e) {
+ log.info(e.toString(), e);
+ return;
+ }
+ DatagramPacket request = new DatagramPacket(new byte[MTU], MTU);
+ for (; ; ) {
+ try {
+ requestsocket.receive(request);
+ handleRequest(requestsocket, request, prefix, proxy, timeout);
+ } catch (Exception e) {
+ log.info(e.toString(), e);
+ }
+ }
+ }
+}
diff --git a/contrib/t2hproxy/build.xml b/contrib/t2hproxy/build.xml
new file mode 100644
index 000000000..5494ab96e
--- /dev/null
+++ b/contrib/t2hproxy/build.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<!--
+ Build file for T2hproxy
+-->
+<project name="T2hproxy" default="jar" basedir=".">
+ <target name="compile">
+ <javac fork="true" srcdir="." destdir="." />
+ </target>
+
+ <target name="jar" depends="compile">
+ <jar jarfile="T2hproxy.jar" basedir="."
+ includes="T2hproxy.class">
+ <manifest>
+ <attribute name="Main-Class" value="T2hproxy" />
+ </manifest>
+ </jar>
+ </target>
+</project>
+
diff --git a/contrib/t2hproxy/runT2proxy.sh b/contrib/t2hproxy/runT2proxy.sh
new file mode 100755
index 000000000..d7fc0d2d2
--- /dev/null
+++ b/contrib/t2hproxy/runT2proxy.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# If the httpclient and logging jars are not in the standard directories
+# edit and uncomment
+# CP='-cp /usr/local/lib/commons-httpclient-2.0-rc1.jar:/usr/local/lib/commons-logging-api.jar:/usr/local/lib/commons-logging.jar'
+
+# Edit and uncomment to use an alternate port
+# PORT='-DT2hproxy.port=1069'
+PREFIX='-DT2hproxy.prefix=http://localhost/'
+# Edit and uncomment to use a proxy
+# PROXY='-DT2hproxy.proxy=localhost:3128'
+# These T2hproxy properties can be put in a file and read in all at once
+# PROPERTIES='-DT2hproxy.properties=t2hproxy.prop
+
+exec java -jar $CP $PORT $PREFIX $PROXY $PROPERTIES T2hproxy.jar
diff --git a/contrib/t2hproxy/t2hproxy.pl b/contrib/t2hproxy/t2hproxy.pl
new file mode 100755
index 000000000..4fc01781b
--- /dev/null
+++ b/contrib/t2hproxy/t2hproxy.pl
@@ -0,0 +1,174 @@
+#!/usr/bin/perl -w
+#
+# tftp to http proxy
+# Copyright 2003 Ken Yap
+# Released under GPL2
+#
+
+require 5.8.0; # needs constant and the pack Z format behaviour
+
+use bytes; # to forestall Unicode interpretation of strings
+use strict;
+
+use Getopt::Long;
+use Socket;
+use Sys::Hostname;
+use Sys::Syslog;
+use LWP;
+use POSIX 'setsid';
+
+use constant PROGNAME => 't2hproxy';
+use constant VERSION => '0.1';
+
+use constant ETH_DATA_LEN => 1500;
+use constant {
+ TFTP_RRQ => 1, TFTP_WRQ => 2, TFTP_DATA => 3, TFTP_ACK => 4,
+ TFTP_ERROR => 5, TFTP_OACK => 6
+};
+use constant {
+ E_UNDEF => 0, E_FNF => 1, E_ACC => 2, E_DISK => 3, E_ILLOP => 4,
+ E_UTID => 5, E_FEXIST => 6, E_NOUSER => 7
+};
+
+use vars qw($prefix $proxy $sockh $timeout %options $tsize $bsize);
+
+# We can't use die because xinetd will think something's wrong
+sub log_and_exit ($) {
+ syslog('info', $_[0]);
+ exit;
+}
+
+sub what_source ($) {
+ my ($port, $saddr) = sockaddr_in($_[0]);
+ my $host = gethostbyaddr($saddr, AF_INET);
+ return ($host, $port);
+}
+
+sub send_error ($$$) {
+ my ($iaddr, $error, $message) = @_;
+ # error packets don't get acked
+ send(STDOUT, pack('nna*', TFTP_ERROR, $error, $message), 0, $iaddr);
+}
+
+sub send_ack_retry ($$$$$) {
+ my ($iaddr, $udptimeout, $maxretries, $blockno, $sendfunc) = @_;
+RETRY:
+ while ($maxretries-- > 0) {
+ &$sendfunc;
+ my $rin = '';
+ my $rout = '';
+ vec($rin, fileno($sockh), 1) = 1;
+ do {
+ my ($fds, $timeleft) = select($rout = $rin, undef, undef, $udptimeout);
+ last if ($fds <= 0);
+ my $ack;
+ my $theiripaddr = recv($sockh, $ack, 256, 0);
+ # check it's for us
+ if ($theiripaddr eq $iaddr) {
+ my ($opcode, $ackblock) = unpack('nn', $ack);
+ return (0) if ($opcode == TFTP_ERROR);
+ # check that the right block was acked
+ if ($ackblock == $blockno) {
+ return (1);
+ } else {
+ syslog('info', "Resending block $blockno");
+ next RETRY;
+ }
+ }
+ # stray packet for some other server instance
+ send_error($theiripaddr, E_UTID, 'Wrong TID');
+ } while (1);
+ }
+ return (0);
+}
+
+sub handle_options ($$) {
+ my ($iaddr, $operand) = @_;
+ while ($operand ne '') {
+ my ($key, $value) = unpack('Z*Z*', $operand);
+ $options{$key} = $value;
+ syslog('info', "$key=$value");
+ $operand = substr($operand, length($key) + length($value) + 2);
+ }
+ my $optstr = '';
+ if (exists($options{blksize})) {
+ $bsize = $options{blksize};
+ $bsize = 512 if ($bsize < 512);
+ $bsize = 1432 if ($bsize > 1432);
+ $optstr .= pack('Z*Z*', 'blksize', $bsize . '');
+ }
+ # OACK expects an ack for block 0
+ log_and_exit('Abort received or retransmit limit reached, exiting')
+ unless send_ack_retry($iaddr, 2, 5, 0,
+ sub { send($sockh, pack('na*', TFTP_OACK, $optstr), 0, $iaddr); });
+}
+
+sub http_get ($) {
+ my ($url) = @_;
+ syslog('info', "GET $url");
+ my $ua = LWP::UserAgent->new;
+ $ua->timeout($timeout);
+ $ua->proxy(['http', 'ftp'], $proxy) if (defined($proxy) and $proxy);
+ my $req = HTTP::Request->new(GET => $url);
+ my $res = $ua->request($req);
+ return ($res->is_success, $res->status_line, $res->content_ref);
+}
+
+sub send_file ($$) {
+ my ($iaddr, $contentref) = @_;
+ my $blockno = 1;
+ my $data;
+ do {
+ $blockno &= 0xffff;
+ $data = substr($$contentref, ($blockno - 1) * $bsize, $bsize);
+ # syslog('info', "Block $blockno length " . length($data));
+ log_and_exit('Abort received or retransmit limit reached, exiting')
+ unless send_ack_retry($iaddr, 2, 5, $blockno,
+ sub { send($sockh, pack('nna*', TFTP_DATA, $blockno, $data), 0, $iaddr); });
+ $blockno++;
+ } while (length($data) >= $bsize);
+}
+
+sub do_rrq ($$) {
+ my ($iaddr, $packetref) = @_;
+ # fork and handle request in child so that *inetd can continue
+ # to serve incoming requests
+ defined(my $pid = fork) or log_and_exit("Can't fork: $!");
+ exit if $pid; # parent exits
+ setsid or log_and_exit("Can't start a new session: $!");
+ socket(SOCK, PF_INET, SOCK_DGRAM, getprotobyname('udp')) or log_and_exit('Cannot create UDP socket');
+ $sockh = *SOCK{IO};
+ my ($opcode, $operand) = unpack('na*', $$packetref);
+ my ($filename, $mode) = unpack('Z*Z*', $operand);
+ syslog('info', "RRQ $filename $mode");
+ my $length = length($filename) + length($mode) + 2;
+ $operand = substr($operand, $length);
+ handle_options($iaddr, $operand) if ($operand ne '');
+ my ($success, $status_line, $result) = http_get($prefix . $filename);
+ syslog('info', $status_line);
+ if ($success) {
+ send_file($iaddr, $result);
+ } else {
+ send_error($iaddr, E_FNF, $status_line);
+ }
+}
+
+$prefix = 'http://localhost/';
+$timeout = 60;
+GetOptions('prefix=s' => \$prefix,
+ 'proxy=s' => \$proxy,
+ 'timeout=i' => \$timeout);
+$bsize = 512;
+openlog(PROGNAME, 'cons,pid', 'user');
+syslog('info', PROGNAME . ' version ' . VERSION);
+my $packet;
+my $theiriaddr = recv(STDIN, $packet, ETH_DATA_LEN, 0);
+my ($host, $port) = what_source($theiriaddr);
+syslog('info', "Connection from $host:$port");
+my $opcode = unpack('n', $packet);
+if ($opcode == TFTP_RRQ) {
+ do_rrq($theiriaddr, \$packet);
+} else { # anything else is an error
+ send_error($theiriaddr, E_ILLOP, 'Illegal operation');
+}
+exit 0;
diff --git a/contrib/t2hproxy/t2hproxy.xinetd b/contrib/t2hproxy/t2hproxy.xinetd
new file mode 100644
index 000000000..ea6a03f1e
--- /dev/null
+++ b/contrib/t2hproxy/t2hproxy.xinetd
@@ -0,0 +1,29 @@
+# Description: tftp to http proxy
+# A sample config file for xinetd, edit and put in /etc/xinetd.d
+# then killall -HUP xinetd, or restart xinetd
+
+service t2hproxy
+{
+ type = UNLISTED
+ id = t2hproxy
+ socket_type = dgram
+ protocol = udp
+#
+# The pathname to where you have installed it
+#
+ server = /usr/local/sbin/t2hproxy.pl
+#
+# If your filenames don't start with /, then the trailing
+# slash is needed
+#
+ server_args = --prefix http://localhost/
+#
+# --proxy http://proxyhost:3128/ can also be appended
+#
+ log_type = FILE /var/log/t2hproxy.log
+ user = nobody
+ wait = yes
+ instances = 10
+ disable = no
+ port = 69
+}