Merge branch 'matiasdoyle-master'

I just noticed that Matias forked this library on Github and ran with it.
He's added a bunch of additional functionality, which I'm happily merging back
into my branch.
This commit is contained in:
Dave Pifke 2017-12-20 23:41:24 +00:00
commit d8b63b5cd9
No known key found for this signature in database
GPG Key ID: 3685742996D9D533
3 changed files with 392 additions and 5 deletions

View File

@ -58,6 +58,7 @@ 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
@ -72,7 +73,7 @@ func Unixgram(ifName string) (Conn, error) {
local, err := ioutil.TempFile("/tmp", "wpa_supplicant")
if err != nil {
panic(err)
return nil, err
}
os.Remove(local.Name())
@ -91,12 +92,15 @@ func Unixgram(ifName string) (Conn, error) {
uc.solicited = make(chan message)
uc.unsolicited = make(chan message)
uc.wpaEvents = make(chan WPAEvent)
go uc.readLoop()
// TODO: issue an ACCEPT command so as to receive unsolicited
// messages. (We don't do this yet, since we don't yet have any way
// to consume them.)
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
}
@ -160,6 +164,47 @@ func (uc *unixgramConn) readLoop() {
}
}
// 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
@ -199,6 +244,18 @@ func (err *ParseError) Error() string {
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 {
@ -211,6 +268,73 @@ func (uc *unixgramConn) Ping() error {
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 {
@ -220,6 +344,130 @@ func (uc *unixgramConn) ScanResults() ([]ScanResult, []error) {
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]
}
}
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) {

View File

@ -131,3 +131,37 @@ func TestParseScanResults(t *testing.T) {
}
}
}
func TestParseStatusResults(t *testing.T) {
testData := "bssid=02:00:01:02:03:04\n" +
"ssid=test network\n" +
"pairwise_cipher=CCMP\n" +
"group_cipher=CCMP\n" +
"key_mgmt=WPA-PSK\n" +
"wpa_state=COMPLETED\n" +
"ip_address=192.168.1.21\n" +
"Supplicant PAE state=AUTHENTICATED\n" +
"suppPortStatus=Authorized\n" +
"EAP state=SUCCESS"
res, err := parseStatusResults(bytes.NewBufferString(testData))
if err != nil {
t.Errorf("Error parsing status result %t", err)
}
if res.WPAState() != "COMPLETED" {
t.Errorf("WPAState was not COMPLETED. Was %s", res.WPAState())
}
if res.IPAddr() != "192.168.1.21" {
t.Errorf("IPAddr was not 192.168.1.21. Was %s", res.IPAddr())
}
if res.KeyMgmt() != "WPA-PSK" {
t.Errorf("KeyMgmt was not WPA-PSK. Was %s", res.KeyMgmt())
}
if res.Address() != "" {
t.Errorf("Address should be empty. Was %s", res.Address())
}
}

View File

@ -120,15 +120,120 @@ func (r *scanResult) Frequency() int { return r.frequency }
func (r *scanResult) RSSI() int { return r.rssi }
func (r *scanResult) Flags() []string { return r.flags }
// ConfiguredNetwork is a configured network (from LIST_NETWORKS)
type ConfiguredNetwork interface {
NetworkID() string
SSID() string
BSSID() string
Flags() []string
}
type configuredNetwork struct {
networkID string
ssid string
bssid string // Since bssid can be any
flags []string
}
func (r *configuredNetwork) NetworkID() string { return r.networkID }
func (r *configuredNetwork) BSSID() string { return r.bssid }
func (r *configuredNetwork) SSID() string { return r.ssid }
func (r *configuredNetwork) Flags() []string { return r.flags }
type StatusResult interface {
WPAState() string
KeyMgmt() string
IPAddr() string
SSID() string
Address() string
}
type statusResult struct {
wpaState string
keyMgmt string
ipAddr string
ssid string
address string
}
func (s *statusResult) WPAState() string { return s.wpaState }
func (s *statusResult) KeyMgmt() string { return s.keyMgmt }
func (s *statusResult) IPAddr() string { return s.ipAddr }
func (s *statusResult) SSID() string { return s.ssid }
func (s *statusResult) Address() string { return s.address }
type WPAEvent struct {
Event string
Arguments map[string]string
Line string
}
// Conn is a connection to wpa_supplicant over one of its communication
// channels.
type Conn interface {
// Close closes the unixgram connection
Close() error
// Ping tests the connection. It returns nil if wpa_supplicant is
// responding.
Ping() error
// AddNetwork creates an empty network configuration. Returns the network
// ID.
AddNetwork() (int, error)
// SetNetwork configures a network property. Returns error if the property
// configuration failed.
SetNetwork(int, string, string) error
// EnableNetwork enables a network. Returns error if the command fails.
EnableNetwork(int) error
// EnableAllNetworks enables all configured networks. Returns error if the command fails.
EnableAllNetworks() error
// SelectNetwork selects a network (and disables the others).
SelectNetwork(int) error
// DisableNetwork disables a network.
DisableNetwork(int) error
// RemoveNetwork removes a network from the configuration.
RemoveNetwork(int) error
// RemoveAllNetworks removes all networks (basically running `REMOVE_NETWORK all`).
// Returns error if command fails.
RemoveAllNetworks() error
// SaveConfig stores the current network configuration to disk.
SaveConfig() error
// Reconfigure sends a RECONFIGURE command to the wpa_supplicant. Returns error when
// command fails.
Reconfigure() error
// Reassociate sends a REASSOCIATE command to the wpa_supplicant. Returns error when
// command fails.
Reassociate() error
// Reconnect sends a RECONNECT command to the wpa_supplicant. Returns error when
// command fails.
Reconnect() error
// ListNetworks returns the currently configured networks.
ListNetworks() ([]ConfiguredNetwork, error)
// Status returns current wpa_supplicant status
Status() (StatusResult, error)
// Scan triggers a new scan. Returns error if the wpa_supplicant does not
// return OK.
Scan() error
// ScanResult returns the latest scanning results. It returns a slice
// of scanned BSSs, and/or a slice of errors representing problems
// communicating with wpa_supplicant or parsing its output.
ScanResults() ([]ScanResult, []error)
EventQueue() chan WPAEvent
}