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 }