github.com/rck/u-root@v0.0.0-20180106144920-7eb602e381bb/pkg/pxe/pxe.go (about)

     1  // Package pxe aims to implement the PXE specification.
     2  //
     3  // See http://www.pix.net/software/pxeboot/archive/pxespec.pdf
     4  package pxe
     5  
     6  import (
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/url"
    14  	"path"
    15  	"strings"
    16  )
    17  
    18  var (
    19  	// ErrConfigNotFound is returned when no suitable configuration file
    20  	// was found by AppendFile.
    21  	ErrConfigNotFound = errors.New("configuration file was not found")
    22  
    23  	// ErrDefaultEntryNotFound is returned when the configuration file
    24  	// names a default label that is not part of the configuration.
    25  	ErrDefaultEntryNotFound = errors.New("default label not found in configuration")
    26  )
    27  
    28  // Entry encapsulates a Syslinux "label" config directive.
    29  type Entry struct {
    30  	// Kernel is the kernel for this label.
    31  	Kernel io.Reader
    32  
    33  	// Initrd is the initial ramdisk for this label.
    34  	Initrd io.Reader
    35  
    36  	// Cmdline is the list of kernel command line parameters.
    37  	Cmdline string
    38  }
    39  
    40  // Config encapsulates a parsed Syslinux configuration file.
    41  //
    42  // See http://www.syslinux.org/wiki/index.php?title=Config for the
    43  // configuration file specification.
    44  //
    45  // TODO: Tear apart parser internals from Config.
    46  type Config struct {
    47  	// Entries is a map of label name -> label configuration.
    48  	Entries map[string]*Entry
    49  
    50  	// DefaultEntry is the default label key to use.
    51  	//
    52  	// If DefaultEntry is non-empty, the label is guaranteed to exist in
    53  	// `Entries`.
    54  	DefaultEntry string
    55  
    56  	// Parser internals.
    57  	globalAppend string
    58  	scope        scope
    59  	curEntry     string
    60  	wd           *url.URL
    61  	schemes      Schemes
    62  }
    63  
    64  type scope uint8
    65  
    66  const (
    67  	scopeGlobal scope = iota
    68  	scopeEntry
    69  )
    70  
    71  // NewConfig returns a new PXE parser using working directory `wd` and default
    72  // schemes.
    73  //
    74  // See NewConfigWithSchemes for more details.
    75  func NewConfig(wd *url.URL) *Config {
    76  	return NewConfigWithSchemes(wd, DefaultSchemes)
    77  }
    78  
    79  // NewConfigWithSchemes returns a new PXE parser using working directory `wd`
    80  // and schemes `s`.
    81  //
    82  // If a path encountered in a configuration file is relative instead of a full
    83  // URI, `wd` is used as the "working directory" of that relative path; the
    84  // resulting URI is roughly `wd.String()/path`.
    85  //
    86  // `s` is used to get files referred to by URIs.
    87  func NewConfigWithSchemes(wd *url.URL, s Schemes) *Config {
    88  	return &Config{
    89  		Entries: make(map[string]*Entry),
    90  		scope:   scopeGlobal,
    91  		wd:      wd,
    92  		schemes: s,
    93  	}
    94  }
    95  
    96  // FindConfigFile probes for config files based on the Mac and IP given.
    97  func (c *Config) FindConfigFile(mac net.HardwareAddr, ip net.IP) error {
    98  	for _, relname := range probeFiles(mac, ip) {
    99  		if err := c.AppendFile(relname); err != ErrConfigNotFound {
   100  			return err
   101  		}
   102  	}
   103  	return fmt.Errorf("no valid pxelinux config found")
   104  }
   105  
   106  // ParseConfigFile parses a PXE/Syslinux configuration as specified in
   107  // http://www.syslinux.org/wiki/index.php?title=Config
   108  //
   109  // Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
   110  // directives are partially supported.
   111  //
   112  // `wd` is the default scheme, host, and path for any files named as a
   113  // relative path. The default path for config files is assumed to be
   114  // `wd.Path`/pxelinux.cfg/.
   115  func ParseConfigFile(uri string, wd *url.URL) (*Config, error) {
   116  	c := NewConfig(wd)
   117  	if err := c.AppendFile(uri); err != nil {
   118  		return nil, err
   119  	}
   120  	return c, nil
   121  }
   122  
   123  // AppendFile parses the config file downloaded from `uri` and adds it to `c`.
   124  func (c *Config) AppendFile(uri string) error {
   125  	cfgWd := *c.wd
   126  	// The default location for looking for configuration is
   127  	// CWD/pxelinux.cfg/, so if `uri` is just a relative path, this will be
   128  	// the default directory.
   129  	cfgWd.Path = path.Join(cfgWd.Path, "pxelinux.cfg")
   130  
   131  	r, err := c.schemes.GetFile(uri, &cfgWd)
   132  	if err != nil {
   133  		return ErrConfigNotFound
   134  	}
   135  	config, err := ioutil.ReadAll(r)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	return c.Append(string(config))
   140  }
   141  
   142  // Append parses `config` and adds the respective configuration to `c`.
   143  func (c *Config) Append(config string) error {
   144  	// Here's a shitty parser.
   145  	for _, line := range strings.Split(config, "\n") {
   146  		// This is stupid. There should be a FieldsN(...).
   147  		kv := strings.Fields(line)
   148  		if len(kv) <= 1 {
   149  			continue
   150  		}
   151  		directive := strings.ToLower(kv[0])
   152  		var arg string
   153  		if len(kv) == 2 {
   154  			arg = kv[1]
   155  		} else {
   156  			arg = strings.Join(kv[1:], " ")
   157  		}
   158  
   159  		switch directive {
   160  		case "default":
   161  			c.DefaultEntry = arg
   162  
   163  		case "include":
   164  			if err := c.AppendFile(arg); err == ErrConfigNotFound {
   165  				// TODO: What to do in this case?
   166  				// Return for now. Test this with pxelinux.
   167  				return err
   168  			} else if err != nil {
   169  				return err
   170  			}
   171  
   172  		case "label":
   173  			// We forever enter label scope.
   174  			c.scope = scopeEntry
   175  			c.curEntry = arg
   176  			c.Entries[c.curEntry] = &Entry{}
   177  			c.Entries[c.curEntry].Cmdline = c.globalAppend
   178  
   179  		case "kernel":
   180  			k, err := c.schemes.LazyGetFile(arg, c.wd)
   181  			if err != nil {
   182  				return err
   183  			}
   184  			c.Entries[c.curEntry].Kernel = k
   185  
   186  		case "initrd":
   187  			i, err := c.schemes.LazyGetFile(arg, c.wd)
   188  			if err != nil {
   189  				return err
   190  			}
   191  			c.Entries[c.curEntry].Initrd = i
   192  
   193  		case "append":
   194  			switch c.scope {
   195  			case scopeGlobal:
   196  				c.globalAppend = arg
   197  
   198  			case scopeEntry:
   199  				if arg == "-" {
   200  					c.Entries[c.curEntry].Cmdline = ""
   201  				} else {
   202  					c.Entries[c.curEntry].Cmdline = arg
   203  				}
   204  			}
   205  		}
   206  	}
   207  
   208  	// Go through all labels and download the initrds.
   209  	for _, label := range c.Entries {
   210  		// If the initrd was set via the INITRD directive, don't
   211  		// overwrite that.
   212  		//
   213  		// TODO(hugelgupf): Is this really what syslinux does? Does
   214  		// INITRD trump cmdline? Does it trump global? What if both the
   215  		// directive and cmdline initrd= are set? Does it depend on the
   216  		// order in the config file? (My current best guess: order.)
   217  		if label.Initrd != nil {
   218  			continue
   219  		}
   220  
   221  		for _, opt := range strings.Fields(label.Cmdline) {
   222  			optkv := strings.Split(opt, "=")
   223  			if optkv[0] != "initrd" {
   224  				continue
   225  			}
   226  
   227  			i, err := c.schemes.LazyGetFile(optkv[1], c.wd)
   228  			if err != nil {
   229  				return err
   230  			}
   231  			label.Initrd = i
   232  		}
   233  	}
   234  
   235  	if len(c.DefaultEntry) > 0 {
   236  		if _, ok := c.Entries[c.DefaultEntry]; !ok {
   237  			return ErrDefaultEntryNotFound
   238  		}
   239  	}
   240  	return nil
   241  
   242  }
   243  
   244  func probeFiles(ethernetMac net.HardwareAddr, ip net.IP) []string {
   245  	files := make([]string, 0, 10)
   246  	// Skipping client UUID. Figure that out later.
   247  
   248  	// MAC address.
   249  	files = append(files, fmt.Sprintf("01-%s", strings.ToLower(strings.Replace(ethernetMac.String(), ":", "-", -1))))
   250  
   251  	// IP address in upper case hex, chopping one letter off at a time.
   252  	ipf := strings.ToUpper(hex.EncodeToString(ip))
   253  	for n := len(ipf); n >= 1; n-- {
   254  		files = append(files, ipf[:n])
   255  	}
   256  	files = append(files, "default")
   257  	return files
   258  }