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
|
|
}
|