github.com/craftyguy/u-root@v1.0.0/pkg/pxe/pxe.go (about)

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