mirror of
				https://github.com/taigrr/golang-wpasupplicant
				synced 2025-01-18 04:43:18 -08:00 
			
		
		
		
	
		
			
				
	
	
		
			559 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			559 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) 2017 Dave Pifke.
 | |
| //
 | |
| // Redistribution and use in source and binary forms, with or without
 | |
| // modification, is permitted provided that the following conditions are met:
 | |
| //
 | |
| // 1. Redistributions of source code must retain the above copyright notice,
 | |
| //    this list of conditions and the following disclaimer.
 | |
| //
 | |
| // 2. Redistributions in binary form must reproduce the above copyright notice,
 | |
| //    this list of conditions and the following disclaimer in the documentation
 | |
| //    and/or other materials provided with the distribution.
 | |
| //
 | |
| // 3. Neither the name of the copyright holder nor the names of its
 | |
| //    contributors may be used to endorse or promote products derived from
 | |
| //    this software without specific prior written permission.
 | |
| //
 | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 | |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 | |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 | |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 | |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 | |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 | |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 | |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 | |
| // POSSIBILITY OF SUCH DAMAGE.
 | |
| 
 | |
| package wpasupplicant
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"syscall"
 | |
| )
 | |
| 
 | |
| // message is a queued response (or read error) from the wpa_supplicant
 | |
| // daemon.  Messages may be either solicited or unsolicited.
 | |
| type message struct {
 | |
| 	priority int
 | |
| 	data     []byte
 | |
| 	err      error
 | |
| }
 | |
| 
 | |
| // unixgramConn is the implementation of Conn for the AF_UNIX SOCK_DGRAM
 | |
| // control interface.
 | |
| //
 | |
| // See https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html.
 | |
| type unixgramConn struct {
 | |
| 	c                      *net.UnixConn
 | |
| 	fd                     uintptr
 | |
| 	solicited, unsolicited chan message
 | |
| 	wpaEvents              chan WPAEvent
 | |
| }
 | |
| 
 | |
| // socketPath is where to find the the AF_UNIX sockets for each interface.  It
 | |
| // can be overridden for testing.
 | |
| var socketPath = "/run/wpa_supplicant"
 | |
| 
 | |
| // Unixgram returns a connection to wpa_supplicant for the specified
 | |
| // interface, using the socket-based control interface.
 | |
| func Unixgram(ifName string) (Conn, error) {
 | |
| 	var err error
 | |
| 	uc := &unixgramConn{}
 | |
| 
 | |
| 	local, err := ioutil.TempFile("/tmp", "wpa_supplicant")
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	os.Remove(local.Name())
 | |
| 
 | |
| 	uc.c, err = net.DialUnix("unixgram",
 | |
| 		&net.UnixAddr{Name: local.Name(), Net: "unixgram"},
 | |
| 		&net.UnixAddr{Name: path.Join(socketPath, ifName), Net: "unixgram"})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	file, err := uc.c.File()
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	uc.fd = file.Fd()
 | |
| 
 | |
| 	uc.solicited = make(chan message)
 | |
| 	uc.unsolicited = make(chan message)
 | |
| 	uc.wpaEvents = make(chan WPAEvent)
 | |
| 
 | |
| 	go uc.readLoop()
 | |
| 	go uc.readUnsolicited()
 | |
| 	// Issue an ATTACH command to start receiving unsolicited events.
 | |
| 	err = uc.runCommand("ATTACH")
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return uc, nil
 | |
| }
 | |
| 
 | |
| // readLoop is spawned after we connect.  It receives messages from the
 | |
| // socket, and routes them to the appropriate channel based on whether they
 | |
| // are solicited (in response to a request) or unsolicited.
 | |
| func (uc *unixgramConn) readLoop() {
 | |
| 	for {
 | |
| 		// The syscall below will block until a datagram is received.
 | |
| 		// It uses a zero-length buffer to look at the datagram
 | |
| 		// without discarding it (MSG_PEEK), returning the actual
 | |
| 		// datagram size (MSG_TRUNC).  See the recvfrom(2) man page.
 | |
| 		//
 | |
| 		// The actual read occurs using UnixConn.Read(), once we've
 | |
| 		// allocated an appropriately-sized buffer.
 | |
| 		n, _, err := syscall.Recvfrom(int(uc.fd), []byte{}, syscall.MSG_PEEK|syscall.MSG_TRUNC)
 | |
| 		if err != nil {
 | |
| 			// Treat read errors as a response to whatever command
 | |
| 			// was last issued.
 | |
| 			uc.solicited <- message{
 | |
| 				err: err,
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		buf := make([]byte, n)
 | |
| 		_, err = uc.c.Read(buf[:])
 | |
| 		if err != nil {
 | |
| 			uc.solicited <- message{
 | |
| 				err: err,
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Unsolicited messages are preceded by a priority
 | |
| 		// specification, e.g. "<1>message".  If there's no priority,
 | |
| 		// default to 2 (info) and assume it's the response to
 | |
| 		// whatever command was last issued.
 | |
| 		var p int
 | |
| 		var c chan message
 | |
| 		if len(buf) >= 3 && buf[0] == '<' && buf[2] == '>' {
 | |
| 			switch buf[1] {
 | |
| 			case '0', '1', '2', '3', '4':
 | |
| 				c = uc.unsolicited
 | |
| 				p, _ = strconv.Atoi(string(buf[1]))
 | |
| 				buf = buf[3:]
 | |
| 			default:
 | |
| 				c = uc.solicited
 | |
| 				p = 2
 | |
| 			}
 | |
| 		} else {
 | |
| 			c = uc.solicited
 | |
| 			p = 2
 | |
| 		}
 | |
| 
 | |
| 		c <- message{
 | |
| 			priority: p,
 | |
| 			data:     buf,
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // readUnsolicited handles messages sent to the unsolicited channel and parse them
 | |
| // into a WPAEvent. At the moment we only handle `CTRL-EVENT-*` events and only events
 | |
| // where the 'payload' is formatted with key=val.
 | |
| func (uc *unixgramConn) readUnsolicited() {
 | |
| 	for {
 | |
| 		mgs := <-uc.unsolicited
 | |
| 		data := bytes.NewBuffer(mgs.data).String()
 | |
| 
 | |
| 		parts := strings.Split(data, " ")
 | |
| 		if len(parts) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if strings.Index(parts[0], "CTRL-") != 0 {
 | |
| 			uc.wpaEvents <- WPAEvent{
 | |
| 				Event: "MESSAGE",
 | |
| 				Line:  data,
 | |
| 			}
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		event := WPAEvent{
 | |
| 			Event:     strings.TrimPrefix(parts[0], "CTRL-EVENT-"),
 | |
| 			Arguments: make(map[string]string),
 | |
| 			Line:      data,
 | |
| 		}
 | |
| 
 | |
| 		for _, args := range parts[1:] {
 | |
| 			if strings.Contains(args, "=") {
 | |
| 				keyval := strings.Split(args, "=")
 | |
| 				if len(keyval) != 2 {
 | |
| 					continue
 | |
| 				}
 | |
| 				event.Arguments[keyval[0]] = keyval[1]
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		uc.wpaEvents <- event
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // cmd executes a command and waits for a reply.
 | |
| func (uc *unixgramConn) cmd(cmd string) ([]byte, error) {
 | |
| 	// TODO: block if any other commands are running
 | |
| 
 | |
| 	_, err := uc.c.Write([]byte(cmd))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	msg := <-uc.solicited
 | |
| 	return msg.data, msg.err
 | |
| }
 | |
| 
 | |
| // ParseError is returned when we can't parse the wpa_supplicant response.
 | |
| // Some functions may return multiple ParseErrors.
 | |
| type ParseError struct {
 | |
| 	// Line is the line of output from wpa_supplicant which we couldn't
 | |
| 	// parse.
 | |
| 	Line string
 | |
| 
 | |
| 	// Err is any nested error.
 | |
| 	Err error
 | |
| }
 | |
| 
 | |
| func (err *ParseError) Error() string {
 | |
| 	b := &bytes.Buffer{}
 | |
| 	b.WriteString("failed to parse wpa_supplicant response")
 | |
| 
 | |
| 	if err.Line != "" {
 | |
| 		fmt.Fprintf(b, ": %q", err.Line)
 | |
| 	}
 | |
| 
 | |
| 	if err.Err != nil {
 | |
| 		fmt.Fprintf(b, ": %s", err.Err.Error())
 | |
| 	}
 | |
| 
 | |
| 	return b.String()
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) EventQueue() chan WPAEvent {
 | |
| 	return uc.wpaEvents
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Close() error {
 | |
| 	if err := uc.runCommand("DETACH"); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return uc.c.Close()
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Ping() error {
 | |
| 	resp, err := uc.cmd("PING")
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if bytes.Compare(resp, []byte("PONG\n")) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return &ParseError{Line: string(resp)}
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) AddNetwork() (int, error) {
 | |
| 	resp, err := uc.cmd("ADD_NETWORK")
 | |
| 	if err != nil {
 | |
| 		return -1, err
 | |
| 	}
 | |
| 
 | |
| 	b := bytes.NewBuffer(resp)
 | |
| 	return strconv.Atoi(strings.Trim(b.String(), "\n"))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) EnableNetwork(networkID int) error {
 | |
| 	return uc.runCommand(fmt.Sprintf("ENABLE_NETWORK %d", networkID))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) EnableAllNetworks() error {
 | |
| 	return uc.runCommand("ENABLE_NETWORK all")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) SelectNetwork(networkID int) error {
 | |
| 	return uc.runCommand(fmt.Sprintf("SELECT_NETWORK %d", networkID))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) DisableNetwork(networkID int) error {
 | |
| 	return uc.runCommand(fmt.Sprintf("DISABLE_NETWORK %d", networkID))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) RemoveNetwork(networkID int) error {
 | |
| 	return uc.runCommand(fmt.Sprintf("REMOVE_NETWORK %d", networkID))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) RemoveAllNetworks() error {
 | |
| 	return uc.runCommand("REMOVE_NETWORK all")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) SetNetwork(networkID int, variable string, value string) error {
 | |
| 	var cmd string
 | |
| 
 | |
| 	// Since key_mgmt expects the value to not be wrapped in "" we do a little check here.
 | |
| 	if variable == "key_mgmt" {
 | |
| 		cmd = fmt.Sprintf("SET_NETWORK %d %s %s", networkID, variable, value)
 | |
| 	} else {
 | |
| 		cmd = fmt.Sprintf("SET_NETWORK %d %s \"%s\"", networkID, variable, value)
 | |
| 	}
 | |
| 
 | |
| 	return uc.runCommand(cmd)
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) SaveConfig() error {
 | |
| 	return uc.runCommand("SAVE_CONFIG")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Reconfigure() error {
 | |
| 	return uc.runCommand("RECONFIGURE")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Reassociate() error {
 | |
| 	return uc.runCommand("REASSOCIATE")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Reconnect() error {
 | |
| 	return uc.runCommand("RECONNECT")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Scan() error {
 | |
| 	return uc.runCommand("SCAN")
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) ScanResults() ([]ScanResult, []error) {
 | |
| 	resp, err := uc.cmd("SCAN_RESULTS")
 | |
| 	if err != nil {
 | |
| 		return nil, []error{err}
 | |
| 	}
 | |
| 
 | |
| 	return parseScanResults(bytes.NewBuffer(resp))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) Status() (StatusResult, error) {
 | |
| 	resp, err := uc.cmd("STATUS")
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return parseStatusResults(bytes.NewBuffer(resp))
 | |
| }
 | |
| 
 | |
| func (uc *unixgramConn) ListNetworks() ([]ConfiguredNetwork, error) {
 | |
| 	resp, err := uc.cmd("LIST_NETWORKS")
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return parseListNetworksResult(bytes.NewBuffer(resp))
 | |
| }
 | |
| 
 | |
| // runCommand is a wrapper around the uc.cmd command which makes sure the
 | |
| // command returned a successful (OK) response.
 | |
| func (uc *unixgramConn) runCommand(cmd string) error {
 | |
| 	resp, err := uc.cmd(cmd)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if bytes.Compare(resp, []byte("OK\n")) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return &ParseError{Line: string(resp)}
 | |
| }
 | |
| 
 | |
| func parseListNetworksResult(resp io.Reader) (res []ConfiguredNetwork, err error) {
 | |
| 	s := bufio.NewScanner(resp)
 | |
| 	if !s.Scan() {
 | |
| 		return nil, &ParseError{}
 | |
| 	}
 | |
| 
 | |
| 	networkIDCol, ssidCol, bssidCol, flagsCol, maxCol := -1, -1, -1, -1, -1
 | |
| 	for n, col := range strings.Split(s.Text(), " / ") {
 | |
| 		switch col {
 | |
| 		case "network id":
 | |
| 			networkIDCol = n
 | |
| 		case "ssid":
 | |
| 			ssidCol = n
 | |
| 		case "bssid":
 | |
| 			bssidCol = n
 | |
| 		case "flags":
 | |
| 			flagsCol = n
 | |
| 		}
 | |
| 
 | |
| 		maxCol = n
 | |
| 	}
 | |
| 
 | |
| 	for s.Scan() {
 | |
| 		ln := s.Text()
 | |
| 		fields := strings.Split(ln, "\t")
 | |
| 		if len(fields) < maxCol {
 | |
| 			return nil, &ParseError{Line: ln}
 | |
| 		}
 | |
| 
 | |
| 		var networkID string
 | |
| 		if networkIDCol != -1 {
 | |
| 			networkID = fields[networkIDCol]
 | |
| 		}
 | |
| 
 | |
| 		var ssid string
 | |
| 		if ssidCol != -1 {
 | |
| 			ssid = fields[ssidCol]
 | |
| 		}
 | |
| 
 | |
| 		var bssid string
 | |
| 		if bssidCol != -1 {
 | |
| 			bssid = fields[bssidCol]
 | |
| 		}
 | |
| 
 | |
| 		var flags []string
 | |
| 		if flagsCol != -1 {
 | |
| 			if len(fields[flagsCol]) >= 2 && fields[flagsCol][0] == '[' && fields[flagsCol][len(fields[flagsCol])-1] == ']' {
 | |
| 				flags = strings.Split(fields[flagsCol][1:len(fields[flagsCol])-1], "][")
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		res = append(res, &configuredNetwork{
 | |
| 			networkID: networkID,
 | |
| 			ssid:      ssid,
 | |
| 			bssid:     bssid,
 | |
| 			flags:     flags,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	return res, nil
 | |
| }
 | |
| 
 | |
| func parseStatusResults(resp io.Reader) (StatusResult, error) {
 | |
| 	s := bufio.NewScanner(resp)
 | |
| 
 | |
| 	res := &statusResult{}
 | |
| 
 | |
| 	for s.Scan() {
 | |
| 		ln := s.Text()
 | |
| 		fields := strings.Split(ln, "=")
 | |
| 		if len(fields) != 2 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		switch fields[0] {
 | |
| 		case "wpa_state":
 | |
| 			res.wpaState = fields[1]
 | |
| 		case "key_mgmt":
 | |
| 			res.keyMgmt = fields[1]
 | |
| 		case "ip_address":
 | |
| 			res.ipAddr = fields[1]
 | |
| 		case "ssid":
 | |
| 			res.ssid = fields[1]
 | |
| 		case "address":
 | |
| 			res.address = fields[1]
 | |
| 		case "bssid":
 | |
| 			res.bssid = fields[1]
 | |
| 		case "freq":
 | |
| 			res.freq = fields[1]
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return res, nil
 | |
| }
 | |
| 
 | |
| // parseScanResults parses the SCAN_RESULTS output from wpa_supplicant.  This
 | |
| // is split out from ScanResults() to make testing easier.
 | |
| func parseScanResults(resp io.Reader) (res []ScanResult, errs []error) {
 | |
| 	// In an attempt to make our parser more resilient, we start by
 | |
| 	// parsing the header line and using that to determine the column
 | |
| 	// order.
 | |
| 	s := bufio.NewScanner(resp)
 | |
| 	if !s.Scan() {
 | |
| 		errs = append(errs, &ParseError{})
 | |
| 		return
 | |
| 	}
 | |
| 	bssidCol, freqCol, rssiCol, flagsCol, ssidCol, maxCol := -1, -1, -1, -1, -1, -1
 | |
| 	for n, col := range strings.Split(s.Text(), " / ") {
 | |
| 		switch col {
 | |
| 		case "bssid":
 | |
| 			bssidCol = n
 | |
| 		case "frequency":
 | |
| 			freqCol = n
 | |
| 		case "signal level":
 | |
| 			rssiCol = n
 | |
| 		case "flags":
 | |
| 			flagsCol = n
 | |
| 		case "ssid":
 | |
| 			ssidCol = n
 | |
| 		}
 | |
| 		maxCol = n
 | |
| 	}
 | |
| 
 | |
| 	var err error
 | |
| 	for s.Scan() {
 | |
| 		ln := s.Text()
 | |
| 		fields := strings.Split(ln, "\t")
 | |
| 		if len(fields) < maxCol {
 | |
| 			errs = append(errs, &ParseError{Line: ln})
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		var bssid net.HardwareAddr
 | |
| 		if bssidCol != -1 {
 | |
| 			if bssid, err = net.ParseMAC(fields[bssidCol]); err != nil {
 | |
| 				errs = append(errs, &ParseError{Line: ln, Err: err})
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var freq int
 | |
| 		if freqCol != -1 {
 | |
| 			if freq, err = strconv.Atoi(fields[freqCol]); err != nil {
 | |
| 				errs = append(errs, &ParseError{Line: ln, Err: err})
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var rssi int
 | |
| 		if rssiCol != -1 {
 | |
| 			if rssi, err = strconv.Atoi(fields[rssiCol]); err != nil {
 | |
| 				errs = append(errs, &ParseError{Line: ln, Err: err})
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var flags []string
 | |
| 		if flagsCol != -1 {
 | |
| 			if len(fields[flagsCol]) >= 2 && fields[flagsCol][0] == '[' && fields[flagsCol][len(fields[flagsCol])-1] == ']' {
 | |
| 				flags = strings.Split(fields[flagsCol][1:len(fields[flagsCol])-1], "][")
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var ssid string
 | |
| 		if ssidCol != -1 {
 | |
| 			ssid = fields[ssidCol]
 | |
| 		}
 | |
| 
 | |
| 		res = append(res, &scanResult{
 | |
| 			bssid:     bssid,
 | |
| 			frequency: freq,
 | |
| 			rssi:      rssi,
 | |
| 			flags:     flags,
 | |
| 			ssid:      ssid,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	return
 | |
| }
 |