├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── ble ├── ble.go ├── client.go └── discover.go ├── cmd ├── root.go ├── server.go └── wifi_connect.go ├── go.mod ├── go.sum ├── main.go ├── pi-wifi.yml ├── public ├── index.html └── qrcode.svg ├── runtime ├── api.go ├── ble_callbacks.go └── runtime.go ├── server ├── server.go └── types.go └── wifi ├── ap.go ├── connect.go ├── connection.go ├── enable.go ├── list.go ├── parser.go ├── parser_test.go ├── status.go └── wifi.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /build 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ARG ARCH=amd64 3 | ADD ./build/${ARCH} /app 4 | ENTRYPOINT ["/app", "server"] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 L.Capra 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build build/amd64 build/arm64 build/arm 2 | 3 | APPNAME ?= pi-wifi 4 | DOCKER_IMAGE ?= opny/${APPNAME} 5 | 6 | BUILD_PATH ?= ./build 7 | 8 | build: build/amd64 build/arm64 build/arm 9 | 10 | build/amd64: 11 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BUILD_PATH}/amd64 . 12 | 13 | build/arm64: 14 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ${BUILD_PATH}/arm64 . 15 | 16 | build/arm: 17 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o ${BUILD_PATH}/arm7 . 18 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o ${BUILD_PATH}/arm6 . 19 | 20 | docker/build: build docker/build/amd64 docker/build/arm64 docker/build/arm 21 | 22 | docker/build/manifest: 23 | 24 | docker manifest push --purge ${DOCKER_IMAGE} || true 25 | 26 | docker manifest create \ 27 | ${DOCKER_IMAGE} \ 28 | --amend ${DOCKER_IMAGE}-amd64 \ 29 | --amend ${DOCKER_IMAGE}-arm64 \ 30 | --amend ${DOCKER_IMAGE}-arm6 \ 31 | --amend ${DOCKER_IMAGE}-arm7 32 | 33 | docker manifest annotate ${DOCKER_IMAGE} ${DOCKER_IMAGE}-amd64 --arch amd64 --os linux 34 | docker manifest annotate ${DOCKER_IMAGE} ${DOCKER_IMAGE}-arm64 --arch arm64 --os linux 35 | docker manifest annotate ${DOCKER_IMAGE} ${DOCKER_IMAGE}-arm6 --arch arm --variant v6 --os linux 36 | docker manifest annotate ${DOCKER_IMAGE} ${DOCKER_IMAGE}-arm7 --arch arm --variant v7 --os linux 37 | 38 | docker manifest push ${DOCKER_IMAGE} 39 | 40 | docker/build/amd64: 41 | docker build . -t ${DOCKER_IMAGE}-amd64 --build-arg ARCH=amd64 42 | 43 | docker/build/arm64: 44 | docker build . -t ${DOCKER_IMAGE}-arm64 --build-arg ARCH=arm64 45 | 46 | docker/build/arm: 47 | docker build . -t ${DOCKER_IMAGE}-arm6 --build-arg ARCH=arm7 48 | docker build . -t ${DOCKER_IMAGE}-arm7 --build-arg ARCH=arm6 49 | 50 | 51 | docker/push: docker/build docker/push/amd64 docker/push/arm64 docker/push/arm docker/build/manifest 52 | docker manifest push ${DOCKER_IMAGE} 53 | 54 | docker/push/amd64: 55 | docker push ${DOCKER_IMAGE}-amd64 56 | 57 | docker/push/arm64: 58 | docker push ${DOCKER_IMAGE}-arm64 59 | 60 | docker/push/arm: 61 | docker push ${DOCKER_IMAGE}-arm6 62 | docker push ${DOCKER_IMAGE}-arm7 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pi-wifi 2 | 3 | Simple WIFI setup over bluetooth 4 | 5 | This servie uses network manager API to connect to a WIFI network. 6 | 7 | The implementation offers two interfaces 8 | 9 | - a bluetooth service 10 | - a rest API for local(host) usage by other services 11 | 12 | ## Example 13 | 14 | You can use `go run main.go` on a linux machine to start the software. 15 | 16 | Then point your browser to http://localhost:9099/ to see an example usage. 17 | 18 | ## Implementation 19 | 20 | ### Bluetooth 21 | 22 | The BLE server exposes one service `12342233-0000-1000-8000-00805f9b34fb` with two characteristics 23 | 24 | 1. `0x3344` that supports 25 | - read - return the connectivity status as enum with values connected, limited, unknown, disconnected 26 | - write - accept a UTF8 string in the format WIFI:T:WPA;S:;P:;H:false; (if ssid or password contains : or ; they must be backslashed eg \; 27 | 2. `0x4455` that support read and list the available APs the wifi device found. The response is in the format `SSID;strength\n` a double \n indicates the end of the list 28 | 29 | ### HTTP API 30 | 31 | The service exposes also an HTTP API to intereact with WIFI connections 32 | 33 | - `/connect` connect to a WIFI connection. Expects a body in the format `{ "payload": "WIFI:T:WPA;S:your ssid;P:your password;H:false;;" }` 34 | - `/status` return the connection status with format `{"status": "connected"}` 35 | - `/listap` list the reachable APs in format `{"accessPoints": [ { "ssid": "example", "strength": 54 } ]}` 36 | 37 | 38 | The connection string format is based on https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 -------------------------------------------------------------------------------- /ble/ble.go: -------------------------------------------------------------------------------- 1 | package ble 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/hw" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/spf13/viper" 7 | 8 | "github.com/muka/go-bluetooth/api/service" 9 | "github.com/muka/go-bluetooth/bluez/profile/agent" 10 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 11 | ) 12 | 13 | // NewService start a GATT server to expose credentials 14 | func NewService() (*service.App, error) { 15 | 16 | adapterID := viper.GetString("ble_adapter") 17 | if adapterID == "" { 18 | adapterID = "hci0" 19 | } else { 20 | log.Infof("Using adapter %s", adapterID) 21 | } 22 | 23 | btmgmt := hw.NewBtMgmt(adapterID) 24 | if viper.GetString("btmgmt_bin") != "" { 25 | log.Infof("Using btmgmt path %s", viper.GetString("btmgmt_bin")) 26 | btmgmt.BinPath = viper.GetString("btmgmt_bin") 27 | } 28 | 29 | // set LE mode 30 | btmgmt.SetPowered(false) 31 | btmgmt.SetLe(true) 32 | btmgmt.SetBredr(false) 33 | btmgmt.SetPowered(true) 34 | 35 | options := service.AppOptions{ 36 | AdapterID: adapterID, 37 | AgentCaps: agent.CapNoInputNoOutput, 38 | UUIDSuffix: viper.GetString("ble_uuid_suffix"), 39 | UUID: viper.GetString("ble_uuid_id"), 40 | } 41 | 42 | a, err := service.NewApp(options) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | a.SetName(viper.GetString("service_name")) 48 | 49 | log.Debugf("HW address %s", a.Adapter().Properties.Address) 50 | 51 | if !a.Adapter().Properties.Powered { 52 | err = a.Adapter().SetPowered(true) 53 | if err != nil { 54 | log.Fatalf("Failed to power the adapter: %s", err) 55 | } 56 | } 57 | 58 | service1, err := a.NewService(viper.GetString("ble_service_id")) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | err = a.AddService(service1) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | // char1 - write connection config and read status 69 | char1, err := service1.NewChar(viper.GetString("ble_char_id_wifi")) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | char1.Properties.Flags = []string{ 75 | gatt.FlagCharacteristicRead, 76 | gatt.FlagCharacteristicWrite, 77 | gatt.FlagCharacteristicNotify, 78 | } 79 | 80 | err = service1.AddChar(char1) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | // char2 - list wifi connections 86 | 87 | char2, err := service1.NewChar(viper.GetString("ble_char_id_ap")) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | char2.Properties.Flags = []string{ 93 | gatt.FlagCharacteristicRead, 94 | gatt.FlagCharacteristicNotify, 95 | } 96 | 97 | err = service1.AddChar(char2) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | return a, nil 103 | } 104 | -------------------------------------------------------------------------------- /ble/client.go: -------------------------------------------------------------------------------- 1 | package ble 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/godbus/dbus/v5" 8 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 9 | "github.com/muka/go-bluetooth/bluez/profile/agent" 10 | "github.com/muka/go-bluetooth/bluez/profile/device" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Client creates a client 15 | func Client(adapterID string, dev *device.Device1) (err error) { 16 | 17 | if adapterID == "" { 18 | return fmt.Errorf("Adapter name not provided") 19 | } 20 | 21 | if dev == nil { 22 | return fmt.Errorf("Device not provided") 23 | } 24 | 25 | //Connect DBus System bus 26 | conn, err := dbus.SystemBus() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // do not reuse agent0 from service 32 | agent.NextAgentPath() 33 | 34 | ag := agent.NewSimpleAgent() 35 | err = agent.ExposeAgent(conn, ag, agent.CapNoInputNoOutput, true) 36 | if err != nil { 37 | return fmt.Errorf("SimpleAgent: %s", err) 38 | } 39 | 40 | // a, err := adapter.GetAdapterFromDevicePath(dev.Path()) 41 | // if err != nil { 42 | // return err 43 | // } 44 | 45 | changes, err := dev.WatchProperties() 46 | go func() { 47 | for { 48 | select { 49 | case ev := <-changes: 50 | log.Infof("updated %s=%v", ev.Name, ev.Value) 51 | 52 | if !dev.Properties.Connected { 53 | err = connect(dev, ag, adapterID) 54 | if err != nil { 55 | log.Errorf("connect err: %s", err) 56 | } 57 | } 58 | 59 | break 60 | } 61 | } 62 | }() 63 | 64 | err = connect(dev, ag, adapterID) 65 | if err != nil { 66 | return err 67 | } 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // retrieveServices(a, dev) 73 | 74 | select {} 75 | // return nil 76 | } 77 | 78 | func connect(dev *device.Device1, ag *agent.SimpleAgent, adapterID string) error { 79 | 80 | props, err := dev.GetProperties() 81 | if err != nil { 82 | return fmt.Errorf("Failed to load props: %s", err) 83 | } 84 | 85 | log.Infof("Found device name=%s addr=%s rssi=%d", props.Name, props.Address, props.RSSI) 86 | 87 | if props.Connected { 88 | log.Trace("Device is connected") 89 | return nil 90 | } 91 | 92 | if !props.Paired || !props.Trusted { 93 | log.Trace("Pairing device") 94 | 95 | err := dev.Pair() 96 | if err != nil { 97 | return fmt.Errorf("Pair failed: %s", err) 98 | } 99 | 100 | log.Info("Pair succeed") 101 | agent.SetTrusted(adapterID, dev.Path()) 102 | } 103 | 104 | if !props.Connected { 105 | log.Info("Connecting device") 106 | err = dev.Connect() 107 | if err != nil { 108 | // if !strings.Contains(err.Error(), "Connection refused") { 109 | return fmt.Errorf("Connect failed: %s", err) 110 | // } 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func retrieveServices(a *adapter.Adapter1, dev *device.Device1) error { 118 | 119 | log.Info("Listing exposed services") 120 | 121 | list, err := dev.GetAllServicesAndUUID() 122 | if err != nil { 123 | return err 124 | } 125 | 126 | if len(list) == 0 { 127 | time.Sleep(time.Second * 2) 128 | return retrieveServices(a, dev) 129 | } 130 | 131 | for _, servicePath := range list { 132 | log.Debugf("%s", servicePath) 133 | } 134 | 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /ble/discover.go: -------------------------------------------------------------------------------- 1 | package ble 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | 7 | "github.com/muka/go-bluetooth/api" 8 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 9 | "github.com/muka/go-bluetooth/bluez/profile/device" 10 | "github.com/muka/go-bluetooth/hw" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | // Discover a device by name 16 | func Discover(adapterID string) error { 17 | 18 | log.SetLevel(log.TraceLevel) 19 | 20 | log.Info("Starting discovery") 21 | 22 | btmgmt := hw.NewBtMgmt(adapterID) 23 | if len(os.Getenv("DOCKER")) > 0 { 24 | btmgmt.BinPath = viper.GetString("btmgmt_bin") 25 | } 26 | 27 | // set LE mode 28 | btmgmt.SetPowered(false) 29 | btmgmt.SetLe(true) 30 | btmgmt.SetBredr(false) 31 | btmgmt.SetDiscoverable(false) 32 | btmgmt.SetPowered(true) 33 | 34 | //clean up connection on exit 35 | defer api.Exit() 36 | 37 | a, err := adapter.GetAdapter(adapterID) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | log.Debug("Flush cached devices") 43 | err = a.FlushDevices() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | log.Debug("Start discovery") 49 | discovery, cancel, err := api.Discover(a, nil) 50 | if err != nil { 51 | return err 52 | } 53 | defer cancel() 54 | 55 | go func() { 56 | devices := map[string]*device.Device1{} 57 | 58 | for ev := range discovery { 59 | 60 | if ev.Type == adapter.DeviceRemoved { 61 | continue 62 | } 63 | 64 | dev, err := device.NewDevice1(ev.Path) 65 | if err != nil { 66 | log.Errorf("%s: %s", ev.Path, err) 67 | continue 68 | } 69 | 70 | if dev == nil { 71 | log.Errorf("%s: not found", ev.Path) 72 | continue 73 | } 74 | 75 | log.Infof("Found name=%s addr=%s rssi=%d", dev.Properties.Name, dev.Properties.Address, dev.Properties.RSSI) 76 | 77 | if _, ok := devices[dev.Properties.Address]; ok { 78 | log.Warnf("Skip duplicated address %s", dev.Properties.Address) 79 | continue 80 | } 81 | 82 | devices[dev.Properties.Address] = dev 83 | 84 | if dev.Properties.Name == viper.GetString("service_name") { 85 | // stop discovery 86 | cancel() 87 | err = Client(adapterID, dev) 88 | } 89 | 90 | } 91 | 92 | }() 93 | 94 | ch := make(chan os.Signal) 95 | signal.Notify(ch, os.Interrupt, os.Kill) // get notified of all OS signals 96 | 97 | sig := <-ch 98 | log.Infof("Received signal [%v]; shutting down...\n", sig) 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var cfgFile string 13 | var adapterID string 14 | 15 | // rootCmd represents the base command when called without any subcommands 16 | var rootCmd = &cobra.Command{ 17 | Use: "pi-wifi", 18 | Short: "", 19 | Long: ``, 20 | // Uncomment the following line if your bare application 21 | // has an action associated with it: 22 | // Run: func(cmd *cobra.Command, args []string) { }, 23 | } 24 | 25 | // Execute adds all child commands to the root command and sets flags appropriately. 26 | // This is called by main.main(). It only needs to happen once to the rootCmd. 27 | func Execute() { 28 | if err := rootCmd.Execute(); err != nil { 29 | fmt.Println(err) 30 | os.Exit(1) 31 | } 32 | } 33 | 34 | func init() { 35 | cobra.OnInitialize(initConfig) 36 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./pi-wifi.yaml)") 37 | rootCmd.PersistentFlags().StringVar(&adapterID, "adapter", "", "the bluetooth adapter to use (default is hci0)") 38 | } 39 | 40 | // initConfig reads in config file and ENV variables if set. 41 | func initConfig() { 42 | if cfgFile != "" { 43 | // Use config file from the flag. 44 | viper.SetConfigFile(cfgFile) 45 | } else { 46 | // Search config in home directory with name ".pi-wifi" (without extension). 47 | viper.AddConfigPath("./") 48 | viper.AddConfigPath("./config") 49 | viper.SetConfigName("pi-wifi") 50 | } 51 | 52 | viper.AutomaticEnv() // read in environment variables that match 53 | 54 | viper.SetDefault("log_level", "info") 55 | viper.SetDefault("service_name", "pi-wifi") 56 | 57 | viper.SetDefault("http_port", 9099) 58 | viper.SetDefault("http_public", true) 59 | viper.SetDefault("http_public_dir", "./public/") 60 | 61 | // viper.SetDefault("btmgmt_bin", "/usr/bin/btmgmt") 62 | viper.SetDefault("ble_adapter", "hci0") 63 | viper.SetDefault("ble_uuid_suffix", "-0000-1000-8000-00805f9b34fb") 64 | viper.SetDefault("ble_uuid_id", "1234") 65 | viper.SetDefault("ble_service_id", "2233") 66 | viper.SetDefault("ble_char_id_wifi", "3344") 67 | viper.SetDefault("ble_char_id_ap", "4455") 68 | 69 | // If a config file is found, read it in. 70 | if err := viper.ReadInConfig(); err == nil { 71 | log.Infof("Using config file %s", viper.ConfigFileUsed()) 72 | } 73 | 74 | lvl, err := log.ParseLevel(viper.GetString("log_level")) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | 79 | log.SetLevel(lvl) 80 | 81 | } 82 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "github.com/muka/pi-wifi/runtime" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // serverCmd represents the server command 11 | var serverCmd = &cobra.Command{ 12 | Use: "server", 13 | Short: "Start a GATT server to enable wifi connection", 14 | Long: ``, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | 17 | instance, err := runtime.NewRuntime() 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | defer instance.Stop() 23 | 24 | err = instance.Start() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | }, 30 | } 31 | 32 | func init() { 33 | rootCmd.AddCommand(serverCmd) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/wifi_connect.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/muka/pi-wifi/wifi" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // wifiConnect represents the server command 11 | var wifiConnect = &cobra.Command{ 12 | Use: "wifi-connect", 13 | Short: "Connect to wifi", 14 | Long: ``, 15 | Run: func(cmd *cobra.Command, args []string) { 16 | 17 | if len(args) < 1 { 18 | log.Fatal("Please provide a connection string eg. WIFI:T:WPA;S:mynetwork;P:mypass;;") 19 | } 20 | 21 | connstr := args[0] 22 | connParams, err := wifi.ParseConnection(connstr) 23 | if err != nil { 24 | log.Fatalf("Failed to parse connection string: %s", err) 25 | } 26 | 27 | manager, err := wifi.NewManager() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | err = manager.Connect(connParams) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | }, 37 | } 38 | 39 | func init() { 40 | rootCmd.AddCommand(wifiConnect) 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muka/pi-wifi 2 | 3 | go 1.14 4 | 5 | replace github.com/muka/go-bluetooth => ../go-bluetooth 6 | 7 | require ( 8 | github.com/godbus/dbus/v5 v5.0.3 9 | github.com/google/uuid v1.1.2 10 | github.com/gorilla/mux v1.8.0 11 | github.com/muka/go-bluetooth v0.0.0 12 | github.com/muka/network_manager v0.0.0-20200903202308-ae5ede816e07 13 | github.com/prometheus/common v0.4.0 14 | github.com/sirupsen/logrus v1.6.0 15 | github.com/spf13/cobra v1.0.0 16 | github.com/spf13/viper v1.7.1 17 | github.com/stretchr/testify v1.6.1 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 17 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 19 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 20 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 21 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 22 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 23 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 24 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 25 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 26 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 27 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 28 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 29 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 30 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 31 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 32 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 33 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 34 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 35 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 36 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 37 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 38 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 39 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 40 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 44 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 45 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 46 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 47 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 48 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 49 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 50 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 51 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 52 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 53 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 54 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 55 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 56 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 57 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 58 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 59 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 60 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 61 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 62 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 63 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 64 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 65 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 69 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 70 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 71 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 72 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 73 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 74 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 75 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 76 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 78 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 79 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 80 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 81 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 82 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 83 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 84 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 85 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 86 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 87 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 88 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 89 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 90 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 91 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 92 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 93 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 94 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 95 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 96 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 97 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 98 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 99 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 100 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 101 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 102 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 103 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 104 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 105 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 106 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 107 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 108 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 109 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 110 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 111 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 112 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 113 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 114 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 115 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 116 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 117 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 118 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 119 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 120 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 121 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 122 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 123 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 124 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 125 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 126 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 127 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 128 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 129 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 130 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 131 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 132 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 133 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 134 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 135 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 136 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 137 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 138 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 139 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 140 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 141 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 142 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 143 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 144 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 145 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 146 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 147 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 148 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 149 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 150 | github.com/muka/go-bluetooth/_examples v0.0.0-20200619025933-f6113f7141c5/go.mod h1:HTx+kHjd+hsVYyNcrB3abQKcUGLOUU3c2fp/fddTkIY= 151 | github.com/muka/network_manager v0.0.0-20200903202308-ae5ede816e07 h1:15hhZ0nE9+aE45xWe04ia3RksvsTeWxHmP4XkelYN0E= 152 | github.com/muka/network_manager v0.0.0-20200903202308-ae5ede816e07/go.mod h1:SPCSAPRdj6CkvCd0k7SRBmWserco0BwVccQA43rD6Ww= 153 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 154 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 155 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 156 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 157 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 158 | github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= 159 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 160 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 161 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 162 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 163 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 164 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 165 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 166 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 167 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 168 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 169 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 170 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 171 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 172 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 173 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= 174 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 175 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 176 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 177 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 178 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 179 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 180 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 181 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 182 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 183 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 184 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= 185 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 186 | github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 187 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 188 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 189 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 190 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 191 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 192 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 193 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 194 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 195 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 196 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 197 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 198 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 199 | github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 200 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 201 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 202 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 203 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 204 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 205 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 206 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 207 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 208 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 209 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 210 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 211 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 212 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 213 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 214 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 215 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 216 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 217 | github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8= 218 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 219 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 220 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 221 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 222 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 223 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 224 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 225 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 226 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 227 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 228 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 229 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 230 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 231 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 232 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 233 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 234 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 235 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= 236 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 237 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 238 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 239 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 240 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 241 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 242 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 243 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 244 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 245 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 246 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 247 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 248 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 249 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 250 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 251 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 252 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 253 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 254 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 255 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 256 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 257 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 258 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 259 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 260 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 261 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 262 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 263 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 264 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 265 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 266 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 267 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 268 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 269 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 270 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 271 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 272 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 273 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 274 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 275 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 276 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 277 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 280 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 281 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 282 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 283 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 284 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 285 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 286 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 287 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 288 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 289 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 290 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= 297 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE= 300 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 302 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 303 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 304 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 305 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 306 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 307 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 308 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 309 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 310 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 311 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 312 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 313 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 314 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 315 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 316 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 317 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 318 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 319 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 320 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 321 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 322 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 323 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 324 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 325 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 326 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 327 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 328 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 329 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 330 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 331 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 332 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 333 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 334 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 335 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 336 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 337 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 338 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 339 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 340 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 341 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 342 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 343 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 344 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 345 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 346 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 347 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 348 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 349 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 350 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 351 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 352 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 353 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 354 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 355 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 356 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 357 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 358 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 359 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 360 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 361 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 362 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 363 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 364 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 365 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 366 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 367 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 368 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 369 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 370 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 371 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 372 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 373 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 374 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 375 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 376 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 377 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 378 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Luca Capra 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import "github.com/muka/pi-wifi/cmd" 19 | 20 | func main() { 21 | cmd.Execute() 22 | } 23 | -------------------------------------------------------------------------------- /pi-wifi.yml: -------------------------------------------------------------------------------- 1 | 2 | service_name: pi-wifi 3 | # btmgmt_bin: /usr/bin/btmgmt 4 | 5 | http_port: 9099 6 | 7 | ble_adapter: hci0 8 | ble_uuid_suffix: -0000-1000-8000-00805f9b34fb 9 | ble_uuid_id: "1234" 10 | 11 | ble_service_id: "2233" 12 | ble_char_id_wifi: "3344" 13 | ble_char_id_ap: "4455" -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bluetooth 6 | 7 | 177 | 199 | 200 | 201 | 202 | 203 | 213 | 214 | ​ 215 | 221 | 222 | 228 | 229 | 235 | 236 | 260 | 261 |
262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /public/qrcode.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 | 1173 | 1174 | 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 1267 | 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1276 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 1309 | 1310 | 1311 | 1312 | 1313 | 1314 | 1315 | 1316 | 1317 | 1318 | 1319 | 1320 | 1321 | 1322 | 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 | 1336 | 1337 | 1338 | 1339 | 1340 | 1341 | 1342 | 1343 | 1344 | 1345 | 1346 | 1347 | 1348 | 1349 | 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 1357 | 1358 | 1359 | 1360 | 1361 | 1362 | 1363 | 1364 | 1365 | 1366 | 1367 | 1368 | 1369 | 1370 | 1371 | 1372 | 1373 | 1374 | 1375 | 1376 | 1377 | 1378 | 1379 | 1380 | 1381 | 1382 | 1383 | 1384 | 1385 | 1386 | 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | 1396 | 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | 1403 | 1404 | 1405 | 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | 1412 | 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | 1435 | 1436 | 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | 1455 | 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | 1467 | 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | 1476 | 1477 | 1478 | 1479 | 1480 | 1481 | 1482 | 1483 | 1484 | 1485 | 1486 | 1487 | 1488 | 1489 | 1490 | 1491 | 1492 | 1493 | 1494 | 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 1502 | 1503 | 1504 | 1505 | 1506 | 1507 | 1508 | 1509 | 1510 | 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 | 1527 | 1528 | 1529 | 1530 | 1531 | 1532 | 1533 | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | -------------------------------------------------------------------------------- /runtime/api.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "github.com/muka/network_manager" 5 | "github.com/muka/pi-wifi/wifi" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func (r *Runtime) listAP() (list []wifi.AccessPoint, err error) { 10 | 11 | devices, err := r.Wifi.GetWifiDevices() 12 | if err != nil { 13 | log.Errorf("Failed to list wifi devices: %s", err) 14 | return list, err 15 | } 16 | 17 | for _, device := range devices { 18 | aps, err := r.Wifi.GetAccessPoints(device.Path) 19 | if err != nil { 20 | log.Warnf("Error getting access points list for %s: %s", device.Interface, err) 21 | continue 22 | } 23 | for _, ap := range aps { 24 | list = append(list, ap) 25 | } 26 | } 27 | 28 | return list, nil 29 | } 30 | 31 | func (r *Runtime) connect(connstr string) (string, error) { 32 | 33 | params, err := wifi.ParseConnection(connstr) 34 | if err != nil { 35 | log.Errorf("Error parsing connection parameters: %s", err) 36 | return "parse_failure", err 37 | } 38 | 39 | err = r.Wifi.Connect(params) 40 | if err != nil { 41 | log.Errorf("Error connecting: %s", err) 42 | return "conn_failure", err 43 | } 44 | 45 | return "ok", nil 46 | } 47 | 48 | func (r *Runtime) getStatus() (string, error) { 49 | 50 | conn, err := r.Wifi.GetConnectivity() 51 | if err != nil { 52 | log.Errorf("Failed to get connectivity: %s", err) 53 | return "", err 54 | } 55 | 56 | res := "disconnected" 57 | switch conn { 58 | case network_manager.NM_CONNECTIVITY_FULL: 59 | res = "connected" 60 | break 61 | case network_manager.NM_CONNECTIVITY_LIMITED: 62 | res = "limited" 63 | break 64 | case network_manager.NM_CONNECTIVITY_UNKNOWN: 65 | res = "unknown" 66 | break 67 | } 68 | 69 | return res, nil 70 | } 71 | -------------------------------------------------------------------------------- /runtime/ble_callbacks.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/muka/go-bluetooth/api/service" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (r *Runtime) onConnectionRead(char *service.Char) { 12 | char.OnRead(service.CharReadCallback(func(c *service.Char, options map[string]interface{}) ([]byte, error) { 13 | log.Debug("onConnectionRead callback") 14 | res, err := r.getStatus() 15 | return []byte(res), err 16 | })) 17 | } 18 | 19 | func (r *Runtime) onConnectionWrite(char *service.Char) { 20 | char.OnWrite(service.CharWriteCallback(func(c *service.Char, value []byte) ([]byte, error) { 21 | log.Debugf("onConnectionWrite callback: %s", value) 22 | res, err := r.connect(string(value)) 23 | return []byte(res), err 24 | })) 25 | 26 | } 27 | 28 | func (r *Runtime) onAPList(char *service.Char) { 29 | char.OnRead(service.CharReadCallback(func(c *service.Char, options map[string]interface{}) ([]byte, error) { 30 | log.Debug("onAPList callback") 31 | 32 | aps, err := r.listAP() 33 | if err != nil { 34 | log.Errorf("Failed to list wifi devices: %s", err) 35 | return []byte{}, err 36 | } 37 | 38 | list := []string{} 39 | for _, ap := range aps { 40 | list = append(list, fmt.Sprintf("%s;%b\n", ap.SSID, ap.Strength)) 41 | } 42 | 43 | return []byte(strings.Join(list, "\n") + "\n"), nil 44 | })) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "github.com/muka/go-bluetooth/api/service" 5 | "github.com/muka/pi-wifi/ble" 6 | "github.com/muka/pi-wifi/server" 7 | "github.com/muka/pi-wifi/wifi" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | //NewRuntime create a new Runtime instance 12 | func NewRuntime() (instance Runtime, err error) { 13 | 14 | instance.Wifi, err = wifi.NewManager() 15 | if err != nil { 16 | return instance, err 17 | } 18 | 19 | instance.Ble, err = ble.NewService() 20 | if err != nil { 21 | return instance, err 22 | } 23 | 24 | services := instance.Ble.GetServices() 25 | for _, service := range services { 26 | for _, char := range service.GetChars() { 27 | switch char.UUID[4:8] { 28 | // connection 29 | case viper.GetString("ble_char_id_wifi"): 30 | instance.onConnectionRead(char) 31 | instance.onConnectionWrite(char) 32 | break 33 | // ap list 34 | case viper.GetString("ble_char_id_ap"): 35 | instance.onAPList(char) 36 | break 37 | } 38 | } 39 | } 40 | 41 | instance.HTTP = server.NewHTTPServer( 42 | // connect 43 | func(connstr string) (string, error) { 44 | return instance.connect(connstr) 45 | }, 46 | // status 47 | func() (string, error) { 48 | return instance.getStatus() 49 | }, 50 | // list APs 51 | func() (aps []server.AccessPoint, err error) { 52 | 53 | list, err := instance.listAP() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | for _, ap1 := range list { 59 | aps = append(aps, server.AccessPoint{ 60 | SSID: string(ap1.SSID), 61 | Strength: int(ap1.Strength), 62 | }) 63 | } 64 | 65 | return aps, nil 66 | }, 67 | ) 68 | if err != nil { 69 | return instance, err 70 | } 71 | 72 | return instance, nil 73 | } 74 | 75 | // Runtime handle instances of 76 | type Runtime struct { 77 | Wifi *wifi.Manager 78 | Ble *service.App 79 | HTTP server.HTTPServer 80 | CancelAdvertise func() 81 | } 82 | 83 | // Stop stop the runtime 84 | func (r *Runtime) Stop() { 85 | r.CancelAdvertise() 86 | r.Ble.Close() 87 | } 88 | 89 | // Start initialize the runtime 90 | func (r *Runtime) Start() error { 91 | 92 | // check if wifi is connects 93 | // subscrbe to connectivity changes 94 | // start BLE advertising if not 95 | 96 | err := r.Ble.Run() 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // seems 0 is not working well 102 | // use uint32 max value 103 | 104 | cancel, err := r.Ble.Advertise(4294967295) 105 | 106 | r.CancelAdvertise = cancel 107 | if err != nil { 108 | return err 109 | } 110 | 111 | err = r.HTTP.Serve() 112 | if err != nil { 113 | return err 114 | } 115 | 116 | select {} 117 | } 118 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/gorilla/mux" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | //NewHTTPServer init an http server wrapper 16 | func NewHTTPServer( 17 | onConnect func(string) (string, error), 18 | onStatus func() (string, error), 19 | onListAP func() ([]AccessPoint, error), 20 | ) HTTPServer { 21 | return HTTPServer{ 22 | onConnect: onConnect, 23 | onStatus: onStatus, 24 | onListAP: onListAP, 25 | } 26 | } 27 | 28 | // HTTPServer wraps http server API 29 | type HTTPServer struct { 30 | onConnect func(string) (string, error) 31 | onListAP func() ([]AccessPoint, error) 32 | onStatus func() (string, error) 33 | } 34 | 35 | // Serve starts an http server 36 | func (srv *HTTPServer) Serve() error { 37 | 38 | router := mux.NewRouter().StrictSlash(true) 39 | 40 | router.HandleFunc("/connect", func(w http.ResponseWriter, r *http.Request) { 41 | 42 | body, err := ioutil.ReadAll(r.Body) 43 | if err != nil { 44 | log.Errorf("Failed to read body") 45 | w.WriteHeader(500) 46 | return 47 | } 48 | 49 | if len(body) == 0 { 50 | log.Errorf("Empty request body") 51 | w.WriteHeader(400) 52 | return 53 | } 54 | 55 | msg := connectRequest{} 56 | err = json.Unmarshal(body, &msg) 57 | if err != nil { 58 | log.Errorf("Failed to parse JSON") 59 | w.WriteHeader(400) 60 | return 61 | } 62 | 63 | res, err := srv.onConnect(msg.Payload) 64 | if err != nil { 65 | log.Errorf("Failed to connect: %s", err) 66 | w.WriteHeader(500) 67 | return 68 | } 69 | 70 | w.WriteHeader(200) 71 | fmt.Fprintf(w, `{ "status": "%s" }`, res) 72 | }) 73 | 74 | router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { 75 | 76 | w.WriteHeader(200) 77 | 78 | status, err := srv.onStatus() 79 | if err != nil { 80 | log.Errorf("failed to get status: %s", err) 81 | w.WriteHeader(500) 82 | return 83 | } 84 | 85 | res := statusResponse{ 86 | Status: status, 87 | } 88 | 89 | err = json.NewEncoder(w).Encode(res) 90 | if err != nil { 91 | log.Errorf("failed to encode response: %s", err) 92 | w.WriteHeader(500) 93 | return 94 | } 95 | 96 | }) 97 | 98 | router.HandleFunc("/listap", func(w http.ResponseWriter, r *http.Request) { 99 | 100 | list, err := srv.onListAP() 101 | if err != nil { 102 | log.Errorf("failed to get status: %s", err) 103 | w.WriteHeader(500) 104 | return 105 | } 106 | 107 | res := listAPResponse{ 108 | AccessPoints: list, 109 | } 110 | 111 | err = json.NewEncoder(w).Encode(res) 112 | if err != nil { 113 | log.Errorf("failed to encode response: %s", err) 114 | w.WriteHeader(500) 115 | return 116 | } 117 | 118 | }) 119 | 120 | if viper.GetBool("http_public") { 121 | httpDir := viper.GetString("http_public_dir") 122 | _, err := os.Stat(httpDir) 123 | if !os.IsNotExist(err) { 124 | fs := http.FileServer(http.Dir(httpDir)) 125 | router.PathPrefix("/").Handler(http.StripPrefix("/", fs)) 126 | log.Debugf("Serving static files from %s", httpDir) 127 | } else { 128 | log.Warnf("http_public_dir=%s doesn't exists", httpDir) 129 | } 130 | } 131 | 132 | port := fmt.Sprintf(":%d", viper.GetInt("http_port")) 133 | log.Debugf("Serving on http://localhost%s", port) 134 | return http.ListenAndServe(port, router) 135 | } 136 | -------------------------------------------------------------------------------- /server/types.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type connectRequest struct { 4 | Payload string `json:"payload"` 5 | } 6 | 7 | type statusResponse struct { 8 | Status string `json:"status"` 9 | } 10 | 11 | // AccessPoint json response 12 | type AccessPoint struct { 13 | SSID string `json:"ssid"` 14 | Strength int `json:"strength"` 15 | } 16 | 17 | type listAPResponse struct { 18 | AccessPoints []AccessPoint `json:"accessPoints"` 19 | } 20 | -------------------------------------------------------------------------------- /wifi/ap.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/godbus/dbus/v5" 9 | "github.com/muka/network_manager" 10 | ) 11 | 12 | // AccessPoint wrap AP information 13 | type AccessPoint struct { 14 | SSID []byte 15 | Strength byte 16 | } 17 | 18 | func (m *Manager) scanAccessPoints(devicePath dbus.ObjectPath) error { 19 | 20 | wireless := network_manager.NewNetworkManager_Device_Wireless(m.conn.Object(nmNs, devicePath)) 21 | 22 | // todo: avoid request new scan if scanning is already in progress 23 | 24 | err := wireless.RequestScan(context.Background(), map[string]dbus.Variant{}) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | } 31 | 32 | // GetAccessPoints return a list of Access Points 33 | func (m *Manager) GetAccessPoints(devicePath dbus.ObjectPath) ([]AccessPoint, error) { 34 | 35 | wireless := network_manager.NewNetworkManager_Device_Wireless(m.conn.Object(nmNs, devicePath)) 36 | 37 | list := []AccessPoint{} 38 | 39 | err := m.EnableWifi() 40 | if err != nil { 41 | return list, err 42 | } 43 | 44 | accessPoints, err := wireless.GetAccessPoints(context.Background()) 45 | if err != nil { 46 | return list, err 47 | } 48 | 49 | for _, accessPointPath := range accessPoints { 50 | 51 | accessPoint := network_manager.NewNetworkManager_AccessPoint(m.conn.Object(nmNs, accessPointPath)) 52 | 53 | ssid, err := accessPoint.GetSsid(context.Background()) 54 | if err != nil { 55 | log.Errorf("Error on GetSsid: %s", err) 56 | continue 57 | } 58 | 59 | strength, err := accessPoint.GetStrength(context.Background()) 60 | if err != nil { 61 | log.Errorf("Error on GetStrength: %s", err) 62 | continue 63 | } 64 | 65 | list = append(list, AccessPoint{ 66 | SSID: ssid, 67 | Strength: strength, 68 | }) 69 | } 70 | 71 | return list, err 72 | } 73 | -------------------------------------------------------------------------------- /wifi/connect.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/google/uuid" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/godbus/dbus/v5" 13 | ) 14 | 15 | //ConnectionNamePrefix the prefix for a connection id to identify managed connections 16 | var ConnectionNamePrefix = "piwifi__" 17 | 18 | // Connect to a wifi network 19 | func (m *Manager) Connect(connectionParams ConnectionParams) error { 20 | 21 | if connectionParams.AuthType != AuthTypeWPA { 22 | return errors.New("Only WPA authentication type is supported at this time") 23 | } 24 | 25 | // enable wifi 26 | err := m.EnableWifi() 27 | if err != nil { 28 | return fmt.Errorf("EnableWifi: %s", err) 29 | } 30 | 31 | log.Debugf("Connecting..") 32 | 33 | // get wifi devices 34 | devices, err := m.GetWifiDevices() 35 | 36 | if len(devices) == 0 { 37 | // todo: check if a device can be activated or unblocked 38 | return fmt.Errorf("No WIFI devices available") 39 | } 40 | 41 | device := devices[0] 42 | 43 | // create wifi connection if not exists 44 | connection, err := m.CreateWifiConnection(connectionParams) 45 | if err != nil { 46 | return fmt.Errorf("CreateWifiConnection: %s", err) 47 | } 48 | 49 | // try to connect 50 | _, err = m.activateConnection(device.Path, connection.ObjectPath) 51 | if err != nil { 52 | return fmt.Errorf("activateConnection: %s", err) 53 | } 54 | 55 | log.Debugf("Connection initiated") 56 | 57 | return nil 58 | } 59 | 60 | // CreateWifiConnection create a unique connection or update if already exists 61 | func (m *Manager) CreateWifiConnection(connectionParams ConnectionParams) (conn Connection, err error) { 62 | 63 | connections, err := m.GetConnections() 64 | if err != nil { 65 | return conn, fmt.Errorf("GetConnections: %s", err) 66 | } 67 | 68 | var connection Connection 69 | for _, conn := range connections { 70 | if conn.SSID == connectionParams.SSID { 71 | connection = conn 72 | } 73 | } 74 | 75 | if connection.ID == "" { 76 | 77 | // create wifi connection if not exists 78 | uuid, err := uuid.NewUUID() 79 | if err != nil { 80 | return conn, fmt.Errorf("uuid: %s", err) 81 | } 82 | 83 | connectionConfig := createWifiConnection(uuid.String(), connectionParams) 84 | 85 | _, err = m.settings.AddConnection(context.Background(), connectionConfig) 86 | if err != nil { 87 | return conn, err 88 | } 89 | 90 | connection, err = m.GetConnectionBySSID(fmt.Sprintf("%s_%s", ConnectionNamePrefix, connectionParams.SSID)) 91 | if err != nil { 92 | if !strings.Contains(err.Error(), "No connection by ssid") { 93 | return conn, fmt.Errorf("GetConnectionBySSID: %s", err) 94 | } 95 | } 96 | 97 | log.Tracef("Created connection %s", connection.ID) 98 | } else { 99 | log.Tracef("Connection %s exists", connection.ID) 100 | } 101 | 102 | return connection, nil 103 | } 104 | 105 | func (m *Manager) activateConnection(devicePath, connectionPath dbus.ObjectPath) (dbus.ObjectPath, error) { 106 | 107 | log.Tracef("%s %s", connectionPath, devicePath) 108 | 109 | activeConnections, err := m.GetActiveConnections() 110 | if err != nil { 111 | return dbus.ObjectPath(""), fmt.Errorf("GetActiveConnections: %s", err) 112 | } 113 | 114 | for _, ac := range activeConnections { 115 | if ac.ObjectPath == connectionPath { 116 | log.Tracef("Connection is already active") 117 | return ac.ActiveConnectionPath, nil 118 | } 119 | } 120 | 121 | activeConn, err := m.networkManager.ActivateConnection( 122 | context.Background(), 123 | connectionPath, 124 | devicePath, 125 | dbus.ObjectPath("/"), // select AP automatically 126 | ) 127 | if err != nil { 128 | return dbus.ObjectPath(""), err 129 | } 130 | 131 | //todo: check if active 132 | // log.Printf("Connection activated: %s", activeConn) 133 | 134 | return activeConn, nil 135 | } 136 | 137 | func createWifiConnection(uuid string, connectionParams ConnectionParams) map[string]map[string]dbus.Variant { 138 | 139 | // uuid.FromBytes([]byte(ssid)) 140 | 141 | wifi := map[string]dbus.Variant{ 142 | "ssid": dbus.MakeVariant([]byte(connectionParams.SSID)), 143 | "mode": dbus.MakeVariant("infrastructure"), 144 | } 145 | 146 | conn := map[string]dbus.Variant{ 147 | "type": dbus.MakeVariant("802-11-wireless"), 148 | "uuid": dbus.MakeVariant(uuid), 149 | "id": dbus.MakeVariant(fmt.Sprintf("%s%s", ConnectionNamePrefix, connectionParams.SSID)), 150 | } 151 | 152 | ip4 := map[string]dbus.Variant{ 153 | "method": dbus.MakeVariant("auto"), 154 | } 155 | ip6 := map[string]dbus.Variant{ 156 | "method": dbus.MakeVariant("ignore"), 157 | } 158 | 159 | // todo: handle other connection types 160 | keyMgm := "" 161 | if connectionParams.AuthType == AuthTypeWPA { 162 | keyMgm = "wpa-psk" 163 | } 164 | 165 | wsec := map[string]dbus.Variant{ 166 | "key-mgmt": dbus.MakeVariant(keyMgm), 167 | "auth-alg": dbus.MakeVariant("open"), 168 | "psk": dbus.MakeVariant(connectionParams.Password), 169 | } 170 | 171 | con := map[string]map[string]dbus.Variant{ 172 | "connection": conn, 173 | "802-11-wireless": wifi, 174 | "802-11-wireless-security": wsec, 175 | "ipv4": ip4, 176 | "ipv6": ip6, 177 | } 178 | 179 | return con 180 | } 181 | -------------------------------------------------------------------------------- /wifi/connection.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/godbus/dbus/v5" 10 | "github.com/muka/network_manager" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Connection wrap a managed connection 15 | type Connection struct { 16 | ID string 17 | Type string 18 | SSID string 19 | ObjectPath dbus.ObjectPath 20 | } 21 | 22 | // ActiveConnection wrap an active connection 23 | type ActiveConnection struct { 24 | Connection 25 | ActiveConnectionPath dbus.ObjectPath 26 | } 27 | 28 | // GetConnection retrieve a manged connection by callback filter 29 | func (m *Manager) GetConnection(fn func(conn Connection) bool) (connection Connection, err error) { 30 | 31 | connections, err := m.GetConnections() 32 | if err != nil { 33 | return connection, fmt.Errorf("GetConnections: %s", err) 34 | } 35 | 36 | for _, conn := range connections { 37 | if fn(conn) { 38 | return conn, nil 39 | } 40 | } 41 | 42 | return connection, errors.New("Connection not found") 43 | } 44 | 45 | // GetConnectionBySSID retrieve a manged connection by ssid 46 | func (m *Manager) GetConnectionBySSID(ssid string) (conn Connection, err error) { 47 | if ssid == "" { 48 | return conn, errors.New("Empty SSID provided") 49 | } 50 | conn, err = m.GetConnection(func(conn Connection) bool { 51 | return conn.SSID == ssid 52 | }) 53 | if err != nil { 54 | err = fmt.Errorf("No connection by ssid=%s", ssid) 55 | } 56 | return conn, err 57 | } 58 | 59 | // GetConnectionByID retrieve a manged connection by ID 60 | func (m *Manager) GetConnectionByID(id string) (conn Connection, err error) { 61 | if id == "" { 62 | return conn, errors.New("Empty ID provided") 63 | } 64 | conn, err = m.GetConnection(func(conn Connection) bool { 65 | return conn.ID == id 66 | }) 67 | if err != nil { 68 | err = fmt.Errorf("No connection by id=%s", id) 69 | } 70 | return conn, err 71 | } 72 | 73 | // GetConnections retrieve all WIFI connections managed by the application 74 | func (m *Manager) GetConnections() (connections []Connection, err error) { 75 | 76 | connectionsPath, err := m.settings.GetConnections(context.Background()) 77 | if err != nil { 78 | return connections, err 79 | } 80 | 81 | for _, connectionPath := range connectionsPath { 82 | 83 | connection := network_manager.NewNetworkManager_Settings_Connection( 84 | m.conn.Object(nmNs, connectionPath), 85 | ) 86 | 87 | config, err := connection.GetSettings(context.Background()) 88 | if err != nil { 89 | log.Errorf("Failed read settings for %s", connectionPath) 90 | continue 91 | } 92 | 93 | if connectionInfo, ok := config["connection"]; ok { 94 | if _, ok := connectionInfo["type"]; ok { 95 | if _, ok := connectionInfo["id"]; ok { 96 | 97 | connectionType := connectionInfo["type"].Value().(string) 98 | connectionID := connectionInfo["id"].Value().(string) 99 | 100 | if connectionType == "802-11-wireless" { 101 | if strings.HasPrefix(connectionID, ConnectionNamePrefix) { 102 | 103 | connectionType := connectionInfo["type"].Value().(string) 104 | connectionID := connectionInfo["id"].Value().(string) 105 | connectionSSID := string(config["802-11-wireless"]["ssid"].Value().([]byte)) 106 | 107 | log.Tracef( 108 | "Found connection id=%s type=%s ssid=%s", 109 | connectionID, 110 | connectionType, 111 | connectionSSID, 112 | ) 113 | 114 | connections = append(connections, Connection{ 115 | Type: connectionType, 116 | ID: connectionID, 117 | SSID: connectionSSID, 118 | ObjectPath: connectionPath, 119 | }) 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | return connections, nil 128 | } 129 | 130 | // GetActiveConnections retrieve active WIFI connections managed by the application 131 | func (m *Manager) GetActiveConnections() (connections []ActiveConnection, err error) { 132 | 133 | connectionsPath, err := m.networkManager.GetActiveConnections(context.Background()) 134 | if err != nil { 135 | return connections, err 136 | } 137 | 138 | for _, connectionPath := range connectionsPath { 139 | 140 | connection := network_manager.NewNetworkManager_Connection_Active( 141 | m.conn.Object(nmNs, connectionPath), 142 | ) 143 | 144 | connectionID, err := connection.GetId(context.Background()) 145 | if err != nil { 146 | log.Errorf("Failed read ID for %s", connectionPath) 147 | continue 148 | } 149 | 150 | if strings.HasPrefix(connectionID, ConnectionNamePrefix) { 151 | 152 | conn, err := m.GetConnectionByID(connectionID) 153 | if err != nil { 154 | log.Errorf("Failed to load connection with ID=%s", err) 155 | continue 156 | } 157 | 158 | log.Tracef( 159 | "Found active connection id=%s type=%s ssid=%s", 160 | conn.ID, 161 | conn.Type, 162 | conn.SSID, 163 | ) 164 | 165 | connections = append(connections, ActiveConnection{ 166 | Connection: conn, 167 | ActiveConnectionPath: connectionPath, 168 | }) 169 | } 170 | } 171 | 172 | return connections, nil 173 | } 174 | -------------------------------------------------------------------------------- /wifi/enable.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // IsWifiEnabled return if wifi is enabled 10 | func (m *Manager) IsWifiEnabled() (bool, error) { 11 | enabled, err := m.networkManager.GetWirelessEnabled(context.Background()) 12 | if err != nil { 13 | return false, err 14 | } 15 | return enabled, nil 16 | } 17 | 18 | // EnableWifi unlock wifi via network manager 19 | func (m *Manager) EnableWifi() error { 20 | 21 | enabled, err := m.IsWifiEnabled() 22 | if err != nil { 23 | return err 24 | } 25 | 26 | if !enabled { 27 | log.Debug("Enabling WIFI...") 28 | err := m.networkManager.SetWirelessEnabled(context.Background(), true) 29 | if err != nil { 30 | return err 31 | } 32 | log.Debug("WIFI enabled") 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /wifi/list.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/godbus/dbus/v5" 7 | "github.com/muka/network_manager" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Device wrap a NetworkManager_Device instance 12 | type Device struct { 13 | Interface string 14 | Path dbus.ObjectPath 15 | Device *network_manager.NetworkManager_Device 16 | } 17 | 18 | // GetWifiDevices enumerate WIFI devices 19 | func (m *Manager) GetWifiDevices() ([]Device, error) { 20 | 21 | list := []Device{} 22 | 23 | devices, err := m.networkManager.GetAllDevices(context.Background()) 24 | if err != nil { 25 | return list, err 26 | } 27 | 28 | for _, devicePath := range devices { 29 | device := network_manager.NewNetworkManager_Device(m.conn.Object(nmNs, devicePath)) 30 | 31 | deviceType, err := device.GetDeviceType(context.Background()) 32 | if err != nil { 33 | log.Warnf("Error reading device type %s: %s", devicePath, err) 34 | continue 35 | } 36 | 37 | deviceInterface, err := device.GetInterface(context.Background()) 38 | if err != nil { 39 | log.Warnf("Error reading device interface %s: %s", devicePath, err) 40 | continue 41 | } 42 | 43 | if network_manager.NM_DEVICE_TYPE_WIFI == deviceType { 44 | list = append(list, Device{ 45 | Path: devicePath, 46 | Device: device, 47 | Interface: deviceInterface, 48 | }) 49 | } 50 | } 51 | 52 | return list, nil 53 | } 54 | -------------------------------------------------------------------------------- /wifi/parser.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // AuthType Wifi authentication mechanism 11 | type AuthType string 12 | 13 | const ( 14 | // AuthTypeWEP WEP authentication 15 | AuthTypeWEP = "WEP" 16 | // AuthTypeWPA WPA authentication 17 | AuthTypeWPA = "WPA" 18 | // AuthTypeWPAEAP WPA2-EAP authentication 19 | AuthTypeWPAEAP = "WPA2-EAP" 20 | // AuthTypeNopass no password 21 | AuthTypeNopass = "nopass" 22 | ) 23 | 24 | // ConnectionParams wrap connection information from QRCode like format 25 | type ConnectionParams struct { 26 | // T WPA Authentication type; can be WEP or WPA or WPA2-EAP, or nopass for no password. Or, omit for no password. 27 | AuthType AuthType 28 | // S mynetwork Network SSID. Required. Enclose in double quotes if it is an ASCII name, but could be interpreted as hex (i.e. "ABCD") 29 | SSID string 30 | // P mypass Password, ignored if T is nopass (in which case it may be omitted). Enclose in double quotes if it is an ASCII name, but could be interpreted as hex (i.e. "ABCD") 31 | Password string 32 | // H true Optional. True if the network SSID is hidden. Note this was mistakenly also used to specify phase 2 method in releases up to 4.7.8 / Barcode Scanner 3.4.0. If not a boolean, it will be interpreted as phase 2 method (see below) for backwards-compatibility 33 | Hidden bool 34 | // E TTLS (WPA2-EAP only) EAP method, like TTLS or PWD 35 | TTLS string 36 | // A anon (WPA2-EAP only) Anonymous identity 37 | Anon string 38 | // I myidentity (WPA2-EAP only) Identity 39 | Identity string 40 | // Phase2 PH2 MSCHAPV2 (WPA2-EAP only) Phase 2 method, like MSCHAPV2 41 | Phase2 string 42 | } 43 | 44 | func (c *ConnectionParams) String() string { 45 | 46 | params := fmt.Sprintf( 47 | "WIFI:T:%s;S:%s;P:%s;H:%t;", 48 | c.AuthType, 49 | c.SSID, 50 | c.Password, 51 | c.Hidden, 52 | ) 53 | 54 | if c.AuthType == AuthTypeWPAEAP { 55 | params += fmt.Sprintf( 56 | "E:%s;A:%s;I:%s;PH2:%s;", 57 | c.TTLS, 58 | c.Anon, 59 | c.Identity, 60 | c.Phase2, 61 | ) 62 | } 63 | 64 | return params + ";" 65 | } 66 | 67 | // ParseConnection parse config from string 68 | // eg. WIFI:T:WPA;S:mynetwork;P:mypass;; 69 | func ParseConnection(raw string) (params ConnectionParams, err error) { 70 | 71 | if len(raw) < 10 { 72 | return params, errors.New("Connections string is empty or too short") 73 | } 74 | 75 | if strings.ToUpper(raw[0:4]) != "WIFI" { 76 | return params, errors.New("Missing WIFI: prefix") 77 | } 78 | 79 | raw = raw[5:] 80 | 81 | if raw[len(raw)-2:] == ";;" { 82 | raw = raw[:len(raw)-2] 83 | } 84 | 85 | sectionRegex := regexp.MustCompile(`([^\\];{1})`) 86 | sections := sectionRegex.FindAllStringIndex(raw, -1) 87 | 88 | lastPos := 0 89 | for _, section := range sections { 90 | 91 | part := raw[lastPos : section[1]-1] 92 | lastPos = section[1] 93 | 94 | if len(part) == 0 { 95 | continue 96 | } 97 | 98 | if len(part) > 3 && part[:3] == "PH2" { 99 | params.Phase2 = part[3:] 100 | continue 101 | } 102 | 103 | sectionName := part[:1] 104 | sectionValue := part[2:] 105 | 106 | switch sectionName { 107 | case "T": 108 | var authType AuthType 109 | switch sectionValue { 110 | case "WPA2-EAP": 111 | authType = AuthTypeWPAEAP 112 | break 113 | case "WEP": 114 | authType = AuthTypeWEP 115 | break 116 | case "nopass": 117 | authType = AuthTypeNopass 118 | break 119 | case "WPA": 120 | authType = AuthTypeWPA 121 | break 122 | } 123 | params.AuthType = authType 124 | break 125 | case "S": 126 | params.SSID = sectionValue 127 | break 128 | case "P": 129 | params.Password = sectionValue 130 | break 131 | case "H": 132 | var hidden bool 133 | if sectionValue == "true" { 134 | hidden = true 135 | } 136 | params.Hidden = hidden 137 | break 138 | case "E": 139 | params.TTLS = sectionValue 140 | break 141 | case "A": 142 | params.Anon = sectionValue 143 | break 144 | case "I": 145 | params.Identity = sectionValue 146 | break 147 | } 148 | } 149 | 150 | return params, nil 151 | } 152 | -------------------------------------------------------------------------------- /wifi/parser_test.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func parseConnection(t *testing.T, info ConnectionParams) { 10 | 11 | res, err := ParseConnection(info.String()) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | assert.Equal(t, res.AuthType, info.AuthType) 17 | assert.Equal(t, res.SSID, info.SSID) 18 | assert.Equal(t, res.Password, info.Password) 19 | assert.Equal(t, res.Hidden, info.Hidden) 20 | 21 | } 22 | 23 | func TestParseConnection1(t *testing.T) { 24 | 25 | info := ConnectionParams{ 26 | AuthType: AuthTypeWPA, 27 | SSID: "mynetwork", 28 | Password: "mypass", 29 | } 30 | 31 | parseConnection(t, info) 32 | } 33 | 34 | func TestParseConnection2(t *testing.T) { 35 | 36 | info := ConnectionParams{ 37 | AuthType: AuthTypeWPAEAP, 38 | SSID: `mynetwork\:\;`, 39 | Password: `my\:pass\;`, 40 | } 41 | 42 | parseConnection(t, info) 43 | } 44 | -------------------------------------------------------------------------------- /wifi/status.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | //GetConnectivity return an enum for the connectivity 8 | func (m *Manager) GetConnectivity() (uint32, error) { 9 | return m.networkManager.GetConnectivity(context.Background()) 10 | } 11 | -------------------------------------------------------------------------------- /wifi/wifi.go: -------------------------------------------------------------------------------- 1 | package wifi 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | "github.com/muka/network_manager" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // NM 802-1x configuration settings 10 | // https://developer.gnome.org/NetworkManager/1.0/ref-settings.html 11 | 12 | const nmNs = network_manager.InterfaceNetworkManager 13 | 14 | // NewManager initialiaze an instance of wifi manager 15 | func NewManager() (*Manager, error) { 16 | wifiManager := new(Manager) 17 | 18 | log.Debug("Connecting to system DBus") 19 | conn, err := dbus.SystemBus() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | wifiManager.conn = conn 25 | 26 | wifiManager.networkManager = network_manager.NewNetworkManager( 27 | conn.Object(nmNs, dbus.ObjectPath("/org/freedesktop/NetworkManager")), 28 | ) 29 | 30 | settings := network_manager.NewNetworkManager_Settings( 31 | conn.Object(nmNs, dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")), 32 | ) 33 | 34 | wifiManager.settings = settings 35 | 36 | return wifiManager, nil 37 | } 38 | 39 | // Manager wrap WIFI management functionalities 40 | type Manager struct { 41 | conn *dbus.Conn 42 | networkManager *network_manager.NetworkManager 43 | settings *network_manager.NetworkManager_Settings 44 | } 45 | --------------------------------------------------------------------------------