golang-wpasupplicant/unixgram.go
Matias Doyle becc4580a5
Read and parse unsolicited messages
Currently only handling CTRL-EVENT-* messages and parses them to WPAEvent.
2017-10-16 11:26:17 +02:00

499 lines
12 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 {
panic(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 {
continue
}
event := WPAEvent{
Event: strings.TrimPrefix(parts[0], "CTRL-EVENT-"),
Arguments: make(map[string]string),
}
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 {
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) 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 {
return uc.runCommand(fmt.Sprintf("SET_NETWORK %d %s \"%s\"", networkID, variable, value))
}
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) 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{}
}
fmt.Println("Listing networks")
networkIDCol, ssidCol, bssidCol, flagsCol, maxCol := -1, -1, -1, -1, -1
fmt.Println(strings.Split(s.Text(), " / "))
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
}
fmt.Println(networkIDCol)
for s.Scan() {
ln := s.Text()
fields := strings.Split(ln, "\t")
fmt.Println(fields)
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,
})
// fmt.Println(res)
}
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
}