├── .github └── workflows │ ├── amd64.yml │ ├── armv7.yml │ └── codeql-analysis.yml ├── .gitignore ├── Dockerfile-amd64 ├── Dockerfile-armv7 ├── LICENSE ├── README.md ├── authz.go ├── authz_test.go ├── build.sh ├── changelog.md ├── contrib ├── README.md ├── lepichon_freebox_grafana_dashboard.json └── mcanevet_freebox_grafana_dashboard.json ├── gauges.go ├── getters.go ├── getters_test.go ├── go.mod ├── go.sum ├── main.go └── structs.go /.github/workflows/amd64.yml: -------------------------------------------------------------------------------- 1 | name: amd64 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check Out Repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Login to Docker Hub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | 22 | - name: Set up Docker Buildx 23 | id: buildx 24 | uses: docker/setup-buildx-action@v1 25 | 26 | - name: Build and push 27 | id: docker_build 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | file: ./Dockerfile-amd64 32 | push: true 33 | tags: saphoooo/freebox-exporter 34 | 35 | - name: Image digest 36 | run: echo ${{ steps.docker_build.outputs.digest }} 37 | -------------------------------------------------------------------------------- /.github/workflows/armv7.yml: -------------------------------------------------------------------------------- 1 | name: armv7 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check Out Repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Login to Docker Hub 17 | uses: docker/login-action@v1 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | 22 | - name: Set up Docker Buildx 23 | id: buildx 24 | uses: docker/setup-buildx-action@v1 25 | 26 | - name: Build and push 27 | id: docker_build 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | file: ./Dockerfile-armv7 32 | push: true 33 | tags: saphoooo/freebox-exporter:armv7 34 | 35 | - name: Image digest 36 | run: echo ${{ steps.docker_build.outputs.digest }} 37 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '44 5 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/freebox_exporter 2 | -------------------------------------------------------------------------------- /Dockerfile-amd64: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 2 | 3 | WORKDIR / 4 | 5 | COPY . . 6 | 7 | ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz /usr/local 8 | 9 | RUN set -x && \ 10 | apt update && \ 11 | apt install -y xz-utils && \ 12 | xz -d -c /usr/local/upx-3.95-amd64_linux.tar.xz | \ 13 | tar -xOf - upx-3.95-amd64_linux/upx > /bin/upx && \ 14 | chmod a+x /bin/upx && \ 15 | go get -d -v . && \ 16 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o app . && \ 17 | strip --strip-unneeded app && \ 18 | upx app 19 | 20 | FROM scratch 21 | 22 | LABEL maintainer="stephane.beuret@gmail.com" 23 | 24 | COPY --from=0 app / 25 | 26 | COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 27 | 28 | ENTRYPOINT ["/app"] 29 | -------------------------------------------------------------------------------- /Dockerfile-armv7: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 2 | 3 | WORKDIR / 4 | 5 | COPY . . 6 | 7 | RUN set -x && \ 8 | go get -d -v . && \ 9 | CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags "-w -s" -a -installsuffix cgo -o app . 10 | 11 | FROM scratch 12 | 13 | LABEL maintainer="stephane.beuret@gmail.com" 14 | 15 | COPY --from=0 app / 16 | 17 | COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 18 | 19 | ENTRYPOINT ["/app"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freebox_exporter 2 | 3 | A Prometheus exporter for Freebox stats 4 | 5 | ## Cmds 6 | 7 | `freebox_exporter` 8 | 9 | ## Flags 10 | 11 | - `-endpoint`: Freebox API url (default http://mafreebox.freebox.fr) 12 | - `-listen`: port for Prometheus metrics (default :10001) 13 | - `-debug`: turn on debug mode 14 | - `-fiber`: turn off DSL metric for fiber Freebox 15 | 16 | ## Preview 17 | 18 | Here's what you can get in Prometheus / Grafana with freebox_exporter: 19 | 20 | ![Preview](https://user-images.githubusercontent.com/13923756/54585380-33318800-4a1a-11e9-8e9d-e434f275755c.png) 21 | 22 | # How to use it 23 | 24 | ## Compiled binary 25 | 26 | If you want to compile the binary, you can refer to [this document](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63) which explains how to do it, depending on your OS and architecture. Alternatively, you can use `./build.sh`. 27 | 28 | You can also find the compiled binaries for MacOS, Linux (x86_64, arm64 and arm) and Windows in the release tab. 29 | 30 | ### Quick start 31 | 32 | ``` 33 | ./freebox_exporter 34 | ``` 35 | 36 | ### The following parameters are optional and can be overridden: 37 | 38 | - Freebox API endpoint 39 | 40 | ``` 41 | ./freebox_exporter -endpoint "http://mafreebox.freebox.fr" 42 | ``` 43 | 44 | - Port 45 | 46 | ``` 47 | ./freebox_exporter -listen ":10001" 48 | ``` 49 | 50 | ## Docker 51 | 52 | ### Quick start 53 | 54 | ``` 55 | docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \ 56 | saphoooo/freebox-exporter 57 | ``` 58 | 59 | ### The following parameters are optional and can be overridden: 60 | 61 | - Local token 62 | 63 | Volume allows to save the access token outside of the container to reuse authentication upon an update of the container. 64 | 65 | ``` 66 | docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \ 67 | -e HOME=token -v /path/to/token:/token saphoooo/freebox-exporter 68 | ``` 69 | 70 | - Freebox API endpoint 71 | 72 | ``` 73 | docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 74 | saphoooo/freebox-exporter -endpoint "http://mafreebox.freebox.fr" 75 | ``` 76 | 77 | - Port 78 | 79 | ``` 80 | docker run -d --name freebox-exporter --restart on-failure -p 8080:10001 \ 81 | saphoooo/freebox-exporter 82 | ``` 83 | 84 | ## Caution on first run 85 | 86 | If you launch the application for the first time, you must allow it to access the freebox API. 87 | - The application must be launched from the local network. 88 | - You have to authorize the application from the freebox front panel. 89 | - You have to modify the rights of the application to give it "Modification des réglages de la Freebox" 90 | 91 | Source: https://dev.freebox.fr/sdk/os/ 92 | -------------------------------------------------------------------------------- /authz.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "encoding/hex" 8 | "encoding/json" 9 | "errors" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | // storeToken stores app_token in ~/.freebox_token 19 | func storeToken(token string, authInf *authInfo) error { 20 | err := os.Setenv("FREEBOX_TOKEN", token) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) { 26 | err := ioutil.WriteFile(authInf.myStore.location, []byte(token), 0600) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // retreiveToken gets the token from file and 36 | // load it in environment variable 37 | func retreiveToken(authInf *authInfo) (string, error) { 38 | if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) { 39 | return "", err 40 | } 41 | data, err := ioutil.ReadFile(authInf.myStore.location) 42 | if err != nil { 43 | return "", err 44 | } 45 | err = os.Setenv("FREEBOX_TOKEN", string(data)) 46 | if err != nil { 47 | return "", err 48 | } 49 | return string(data), nil 50 | } 51 | 52 | // getTrackID is the initial request to freebox API 53 | // get app_token and track_id 54 | func getTrackID(authInf *authInfo) (*track, error) { 55 | req, _ := json.Marshal(authInf.myApp) 56 | buf := bytes.NewReader(req) 57 | resp, err := http.Post(authInf.myAPI.authz, "application/json", buf) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | body, err := ioutil.ReadAll(resp.Body) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | trackID := track{} 68 | err = json.Unmarshal(body, &trackID) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | err = storeToken(trackID.Result.AppToken, authInf) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return &trackID, nil 79 | } 80 | 81 | // getGranted waits for user to validate on the freebox front panel 82 | // with a timeout of 15 seconds 83 | func getGranted(authInf *authInfo) error { 84 | trackID, err := getTrackID(authInf) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | url := authInf.myAPI.authz + strconv.Itoa(trackID.Result.TrackID) 90 | for i := 0; i < 15; i++ { 91 | resp, err := http.Get(url) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | defer resp.Body.Close() 97 | body, err := ioutil.ReadAll(resp.Body) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | granted := grant{} 103 | err = json.Unmarshal(body, &granted) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | switch granted.Result.Status { 109 | case "unknown": 110 | return errors.New("the app_token is invalid or has been revoked") 111 | case "pending": 112 | log.Println("the user has not confirmed the authorization request yet") 113 | case "timeout": 114 | return errors.New("the user did not confirmed the authorization within the given time") 115 | case "granted": 116 | log.Println("the app_token is valid and can be used to open a session") 117 | i = 15 118 | case "denied": 119 | return errors.New("the user denied the authorization request") 120 | } 121 | time.Sleep(1 * time.Second) 122 | } 123 | return nil 124 | } 125 | 126 | // getChallenge makes sure the app always has a valid challenge 127 | func getChallenge(authInf *authInfo) (*challenge, error) { 128 | resp, err := http.Get(authInf.myAPI.login) 129 | if err != nil { 130 | return nil, err 131 | } 132 | defer resp.Body.Close() 133 | body, err := ioutil.ReadAll(resp.Body) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | challenged := challenge{} 139 | err = json.Unmarshal(body, &challenged) 140 | if err != nil { 141 | return nil, err 142 | } 143 | return &challenged, nil 144 | } 145 | 146 | // hmacSha1 encodes app_token in hmac-sha1 and stores it in password 147 | func hmacSha1(appToken, challenge string) string { 148 | hash := hmac.New(sha1.New, []byte(appToken)) 149 | hash.Write([]byte(challenge)) 150 | return hex.EncodeToString(hash.Sum(nil)) 151 | } 152 | 153 | // getSession gets a session with freeebox API 154 | func getSession(authInf *authInfo, passwd string) (*sessionToken, error) { 155 | s := session{ 156 | AppID: authInf.myApp.AppID, 157 | Password: passwd, 158 | } 159 | req, err := json.Marshal(s) 160 | if err != nil { 161 | return nil, err 162 | } 163 | buf := bytes.NewReader(req) 164 | resp, err := http.Post(authInf.myAPI.loginSession, "application/json", buf) 165 | if err != nil { 166 | return nil, err 167 | } 168 | body, err := ioutil.ReadAll(resp.Body) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | token := sessionToken{} 174 | err = json.Unmarshal(body, &token) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return &token, nil 179 | } 180 | 181 | // getToken gets a valid session_token and asks for user to change 182 | // the set of permissions on the API 183 | func getToken(authInf *authInfo, xSessionToken *string) (string, error) { 184 | if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) { 185 | err = getGranted(authInf) 186 | if err != nil { 187 | return "", err 188 | } 189 | 190 | reader := authInf.myReader 191 | log.Println("check \"Modification des réglages de la Freebox\" and press enter") 192 | _, err = reader.ReadString('\n') 193 | if err != nil { 194 | return "", err 195 | } 196 | } else { 197 | _, err := retreiveToken(authInf) 198 | if err != nil { 199 | return "", err 200 | } 201 | } 202 | 203 | token, err := getSessToken(os.Getenv("FREEBOX_TOKEN"), authInf, xSessionToken) 204 | if err != nil { 205 | return "", err 206 | } 207 | *xSessionToken = token 208 | return token, nil 209 | } 210 | 211 | // getSessToken gets a new token session when the old one has expired 212 | func getSessToken(token string, authInf *authInfo, xSessionToken *string) (string, error) { 213 | challenge, err := getChallenge(authInf) 214 | if err != nil { 215 | return "", err 216 | } 217 | password := hmacSha1(token, challenge.Result.Challenge) 218 | t, err := getSession(authInf, password) 219 | if err != nil { 220 | return "", err 221 | } 222 | if t.Success == false { 223 | return "", errors.New(t.Msg) 224 | } 225 | *xSessionToken = t.Result.SessionToken 226 | return t.Result.SessionToken, nil 227 | } 228 | -------------------------------------------------------------------------------- /authz_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestRetreiveToken(t *testing.T) { 16 | ai := &authInfo{ 17 | myStore: store{location: "/tmp/token"}, 18 | } 19 | 20 | _, err := retreiveToken(ai) 21 | if err.Error() != "stat /tmp/token: no such file or directory" { 22 | t.Error("Expected bla, but got", err) 23 | } 24 | 25 | ioutil.WriteFile(ai.myStore.location, []byte("IOI"), 0600) 26 | defer os.Remove(ai.myStore.location) 27 | 28 | token, err := retreiveToken(ai) 29 | if err != nil { 30 | t.Error("Expected no err, but got", err) 31 | } 32 | 33 | newToken := os.Getenv("FREEBOX_TOKEN") 34 | 35 | if newToken != "IOI" { 36 | t.Error("Expected IOI, but got", newToken) 37 | } 38 | 39 | if token != "IOI" { 40 | t.Error("Expected IOI, but got", newToken) 41 | } 42 | 43 | os.Unsetenv("FREEBOX_TOKEN") 44 | } 45 | 46 | func TestStoreToken(t *testing.T) { 47 | var token string 48 | 49 | ai := &authInfo{} 50 | token = "IOI" 51 | err := storeToken(token, ai) 52 | if err.Error() != "open : no such file or directory" { 53 | t.Error("Expected open : no such file or directory, but got", err) 54 | } 55 | 56 | ai.myStore.location = "/tmp/token" 57 | err = storeToken(token, ai) 58 | if err != nil { 59 | t.Error("Expected no err, but got", err) 60 | } 61 | defer os.Remove(ai.myStore.location) 62 | 63 | token = os.Getenv("FREEBOX_TOKEN") 64 | if token != "IOI" { 65 | t.Error("Expected IOI, but got", token) 66 | } 67 | os.Unsetenv("FREEBOX_TOKEN") 68 | 69 | data, err := ioutil.ReadFile(ai.myStore.location) 70 | if err != nil { 71 | t.Error("Expected no err, but got", err) 72 | } 73 | 74 | if string(data) != "IOI" { 75 | t.Error("Expected IOI, but got", string(data)) 76 | } 77 | 78 | } 79 | 80 | func TestGetTrackID(t *testing.T) { 81 | ai := &authInfo{ 82 | myStore: store{location: "/tmp/token"}, 83 | } 84 | 85 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 | myTrack := track{ 87 | Success: true, 88 | } 89 | myTrack.Result.AppToken = "IOI" 90 | myTrack.Result.TrackID = 101 91 | result, _ := json.Marshal(myTrack) 92 | fmt.Fprintln(w, string(result)) 93 | })) 94 | defer ts.Close() 95 | 96 | ai.myAPI.authz = ts.URL 97 | trackID, err := getTrackID(ai) 98 | if err != nil { 99 | t.Error("Expected no err, but got", err) 100 | } 101 | defer os.Remove(ai.myStore.location) 102 | defer os.Unsetenv("FREEBOX_TOKEN") 103 | 104 | if trackID.Result.TrackID != 101 { 105 | t.Error("Expected 101, but got", trackID.Result.TrackID) 106 | } 107 | 108 | // as getTrackID have no return value 109 | // the result of storeToken func is checked instead 110 | token := os.Getenv("FREEBOX_TOKEN") 111 | if token != "IOI" { 112 | t.Error("Expected IOI, but got", token) 113 | } 114 | } 115 | 116 | func TestGetGranted(t *testing.T) { 117 | 118 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 119 | switch r.RequestURI { 120 | case "/unknown/": 121 | myTrack := track{ 122 | Success: true, 123 | } 124 | myTrack.Result.TrackID = 101 125 | result, _ := json.Marshal(myTrack) 126 | fmt.Fprintln(w, string(result)) 127 | case "/unknown/101": 128 | myGrant := grant{ 129 | Success: true, 130 | } 131 | myGrant.Result.Status = "unknown" 132 | result, _ := json.Marshal(myGrant) 133 | fmt.Fprintln(w, string(result)) 134 | case "/timeout/": 135 | myTrack := track{ 136 | Success: true, 137 | } 138 | myTrack.Result.TrackID = 101 139 | result, _ := json.Marshal(myTrack) 140 | fmt.Fprintln(w, string(result)) 141 | case "/timeout/101": 142 | myGrant := grant{ 143 | Success: true, 144 | } 145 | myGrant.Result.Status = "timeout" 146 | result, _ := json.Marshal(myGrant) 147 | fmt.Fprintln(w, string(result)) 148 | case "/denied/": 149 | myTrack := track{ 150 | Success: true, 151 | } 152 | myTrack.Result.TrackID = 101 153 | result, _ := json.Marshal(myTrack) 154 | fmt.Fprintln(w, string(result)) 155 | case "/denied/101": 156 | myGrant := grant{ 157 | Success: true, 158 | } 159 | myGrant.Result.Status = "denied" 160 | result, _ := json.Marshal(myGrant) 161 | fmt.Fprintln(w, string(result)) 162 | case "/granted/": 163 | myTrack := track{ 164 | Success: true, 165 | } 166 | myTrack.Result.TrackID = 101 167 | result, _ := json.Marshal(myTrack) 168 | fmt.Fprintln(w, string(result)) 169 | case "/granted/101": 170 | myGrant := grant{ 171 | Success: true, 172 | } 173 | myGrant.Result.Status = "granted" 174 | result, _ := json.Marshal(myGrant) 175 | fmt.Fprintln(w, string(result)) 176 | default: 177 | fmt.Fprintln(w, http.StatusNotFound) 178 | } 179 | })) 180 | defer ts.Close() 181 | 182 | ai := authInfo{} 183 | ai.myAPI.authz = ts.URL + "/unknown/" 184 | ai.myStore.location = "/tmp/token" 185 | 186 | err := getGranted(&ai) 187 | if err.Error() != "the app_token is invalid or has been revoked" { 188 | t.Error("Expected the app_token is invalid or has been revoked, but got", err) 189 | } 190 | defer os.Remove(ai.myStore.location) 191 | defer os.Unsetenv("FREEBOX_TOKEN") 192 | 193 | ai.myAPI.authz = ts.URL + "/timeout/" 194 | err = getGranted(&ai) 195 | if err.Error() != "the user did not confirmed the authorization within the given time" { 196 | t.Error("Expected the user did not confirmed the authorization within the given time, but got", err) 197 | } 198 | 199 | ai.myAPI.authz = ts.URL + "/denied/" 200 | err = getGranted(&ai) 201 | if err.Error() != "the user denied the authorization request" { 202 | t.Error("Expected the user denied the authorization request, but got", err) 203 | } 204 | 205 | ai.myAPI.authz = ts.URL + "/granted/" 206 | err = getGranted(&ai) 207 | if err != nil { 208 | t.Error("Expected no err, but got", err) 209 | } 210 | } 211 | 212 | func TestGetChallenge(t *testing.T) { 213 | 214 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 215 | myChall := &challenge{ 216 | Success: true, 217 | } 218 | myChall.Result.Challenge = "foobar" 219 | result, _ := json.Marshal(myChall) 220 | fmt.Fprintln(w, string(result)) 221 | })) 222 | defer ts.Close() 223 | 224 | ai := &authInfo{ 225 | myAPI: api{ 226 | login: ts.URL, 227 | }, 228 | } 229 | 230 | challenged, err := getChallenge(ai) 231 | if err != nil { 232 | t.Error("Expected no err, but got", err) 233 | } 234 | 235 | if challenged.Success != true { 236 | t.Error("Expected true, but got", challenged.Success) 237 | } 238 | 239 | if challenged.Result.Challenge != "foobar" { 240 | t.Error("Expected foobar, but got", challenged.Result.Challenge) 241 | } 242 | } 243 | 244 | func TestHmacSha1(t *testing.T) { 245 | hmac := hmacSha1("IOI", "foobar") 246 | if hmac != "02fb876a39b64eddcfee3eaa69465cb3e8d53cde" { 247 | t.Error("Expected 02fb876a39b64eddcfee3eaa69465cb3e8d53cde, but got", hmac) 248 | } 249 | } 250 | 251 | func TestGetSession(t *testing.T) { 252 | 253 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 254 | myToken := &sessionToken{ 255 | Success: true, 256 | } 257 | myToken.Result.Challenge = "foobar" 258 | result, _ := json.Marshal(myToken) 259 | fmt.Fprintln(w, string(result)) 260 | })) 261 | defer ts.Close() 262 | 263 | ai := &authInfo{ 264 | myAPI: api{ 265 | loginSession: ts.URL, 266 | }, 267 | } 268 | 269 | token, err := getSession(ai, "") 270 | if err != nil { 271 | t.Error("Expected no err, but got", err) 272 | } 273 | defer os.Unsetenv("FREEBOX_TOKEN") 274 | 275 | if token.Success != true { 276 | t.Error("Expected true, but got", token.Success) 277 | } 278 | 279 | if token.Result.Challenge != "foobar" { 280 | t.Error("Expected foobar, but got", token.Result.Challenge) 281 | } 282 | } 283 | 284 | func TestGetToken(t *testing.T) { 285 | 286 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 287 | switch r.RequestURI { 288 | case "/login": 289 | myChall := &challenge{ 290 | Success: true, 291 | } 292 | myChall.Result.Challenge = "foobar" 293 | result, _ := json.Marshal(myChall) 294 | fmt.Fprintln(w, string(result)) 295 | case "/session": 296 | myToken := sessionToken{ 297 | Success: true, 298 | } 299 | myToken.Result.SessionToken = "foobar" 300 | result, _ := json.Marshal(myToken) 301 | fmt.Fprintln(w, string(result)) 302 | case "/granted/": 303 | myTrack := track{ 304 | Success: true, 305 | } 306 | myTrack.Result.TrackID = 101 307 | result, _ := json.Marshal(myTrack) 308 | fmt.Fprintln(w, string(result)) 309 | case "/granted/101": 310 | myGrant := grant{ 311 | Success: true, 312 | } 313 | myGrant.Result.Status = "granted" 314 | result, _ := json.Marshal(myGrant) 315 | fmt.Fprintln(w, string(result)) 316 | default: 317 | fmt.Fprintln(w, http.StatusNotFound) 318 | } 319 | })) 320 | defer ts.Close() 321 | 322 | ai := authInfo{} 323 | ai.myStore.location = "/tmp/token" 324 | ai.myAPI.login = ts.URL + "/login" 325 | ai.myAPI.loginSession = ts.URL + "/session" 326 | ai.myAPI.authz = ts.URL + "/granted/" 327 | ai.myReader = bufio.NewReader(strings.NewReader("\n")) 328 | 329 | var mySessionToken string 330 | 331 | // the first pass valide getToken without a token stored in a file 332 | tk, err := getToken(&ai, &mySessionToken) 333 | if err != nil { 334 | t.Error("Expected no err, but got", err) 335 | } 336 | defer os.Remove(ai.myStore.location) 337 | defer os.Unsetenv("FREEBOX_TOKEN") 338 | 339 | if mySessionToken != "foobar" { 340 | t.Error("Expected foobar, but got", mySessionToken) 341 | } 342 | 343 | if tk != "foobar" { 344 | t.Error("Expected foobar, but got", tk) 345 | } 346 | 347 | // the second pass validate getToken with a token stored in a file: 348 | // the first pass creates a file at ai.myStore.location 349 | tk, err = getToken(&ai, &mySessionToken) 350 | if err != nil { 351 | t.Error("Expected no err, but got", err) 352 | } 353 | 354 | if mySessionToken != "foobar" { 355 | t.Error("Expected foobar, but got", mySessionToken) 356 | } 357 | 358 | if tk != "foobar" { 359 | t.Error("Expected foobar, but got", tk) 360 | } 361 | 362 | } 363 | 364 | func TestGetSessToken(t *testing.T) { 365 | 366 | myToken := &sessionToken{} 367 | 368 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 369 | switch r.RequestURI { 370 | case "/login": 371 | myChall := &challenge{ 372 | Success: true, 373 | } 374 | myChall.Result.Challenge = "foobar" 375 | result, _ := json.Marshal(myChall) 376 | fmt.Fprintln(w, string(result)) 377 | case "/session": 378 | myToken.Success = true 379 | myToken.Result.SessionToken = "foobar" 380 | result, _ := json.Marshal(myToken) 381 | fmt.Fprintln(w, string(result)) 382 | case "/session2": 383 | myToken.Msg = "failed to get a session" 384 | myToken.Success = false 385 | result, _ := json.Marshal(myToken) 386 | fmt.Fprintln(w, string(result)) 387 | default: 388 | fmt.Fprintln(w, http.StatusNotFound) 389 | } 390 | })) 391 | defer ts.Close() 392 | 393 | ai := authInfo{} 394 | ai.myAPI.login = ts.URL + "/login" 395 | ai.myAPI.loginSession = ts.URL + "/session" 396 | var mySessionToken string 397 | 398 | st, err := getSessToken("token", &ai, &mySessionToken) 399 | if err != nil { 400 | t.Error("Expected no err, but got", err) 401 | } 402 | 403 | if st != "foobar" { 404 | t.Error("Expected foobar, but got", st) 405 | } 406 | 407 | ai.myAPI.loginSession = ts.URL + "/session2" 408 | 409 | _, err = getSessToken("token", &ai, &mySessionToken) 410 | if err.Error() != "failed to get a session" { 411 | t.Error("Expected but got failed to get a session, but got", err) 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | ARCH=${1:-arm} 6 | GOARCH=$ARCH go build -ldflags "-s -w" 7 | ls -lh freebox_exporter 8 | file freebox_exporter 9 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.3] - 2020-10-04 4 | 5 | - Add VPN server metrics, mainly tx and rx for a user on a vpn with scr and local ip as labels 6 | 7 | ## [1.2.2] - 2020-10-03 8 | 9 | - Change variable type int to int64 for RRD metrics, causing an error "constant overflow" on arm chipset 10 | 11 | ## [1.2] - 2020-05-27 12 | 13 | - Add build script 14 | - Log Freebox Server uptime and firmware version 15 | - Log G.INP data 16 | - Log connection status, protocol and modulation 17 | - Log XDSL stats 18 | - Don't log incorrect values (previously logged as zero) 19 | - Remove dead code 20 | 21 | ## [1.1.9] - 2020-04-24 22 | 23 | - Log freeplug speeds and connectivity 24 | - Go 1.14 25 | - Remove Godeps and vendored files 26 | 27 | ## [1.1.7] - 2019-09-04 28 | 29 | - Go 1.13 30 | 31 | ## [1.1.6] - 2019-09-04 32 | 33 | - There is no more uncomfortable error message when the application renews its token 34 | - Adding a `-fiber` flag so that Freebox fiber users do not capture DSL metrics, which are empty on this type of Freebox 35 | 36 | ## [1.1.4] - 2019-09-03 37 | 38 | - Adding a `-debug` flag to have more verbose error logs 39 | 40 | ## [1.1.2] - 2019-08-25 41 | 42 | - Improve error messages 43 | 44 | ## [1.1.1] - 2019-07-31 45 | 46 | - New Dockerfile for amd64 arch: reduce the image size to 3mb 47 | 48 | ## [1.1.0] - 2019-07-31 49 | 50 | - Fix temp metrics 51 | - Add Godeps 52 | 53 | ## [1.0.1] - 2019-07-30 54 | 55 | - Change error catching 56 | 57 | ## [1.0.0] - 2019-07-29 58 | 59 | - Rewriting the application by adding a ton of unit tests 60 | -------------------------------------------------------------------------------- /contrib/README.md: -------------------------------------------------------------------------------- 1 | Here you'll find related contributions to the project, such as ready-to-use Grafana dashboards (this will save you from having to reinvent the wheel when using the Freebox Exporter). 2 | 3 | Thanks to [mcanevet](https://gist.github.com/mcanevet) for his contribution: 4 | 5 | mcanevet Grafana dashbord 6 | 7 | Thanks to [Pichon](https://github.com/lepichon) for his contribution: 8 | 9 | lepichon Grafana dashbord -------------------------------------------------------------------------------- /contrib/lepichon_freebox_grafana_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "6.6.0" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "1.0.0" 30 | }, 31 | { 32 | "type": "panel", 33 | "id": "stat", 34 | "name": "Stat", 35 | "version": "" 36 | } 37 | ], 38 | "annotations": { 39 | "list": [ 40 | { 41 | "builtIn": 1, 42 | "datasource": "-- Grafana --", 43 | "enable": true, 44 | "hide": true, 45 | "iconColor": "rgba(0, 211, 255, 1)", 46 | "name": "Annotations & Alerts", 47 | "type": "dashboard" 48 | } 49 | ] 50 | }, 51 | "editable": true, 52 | "gnetId": null, 53 | "graphTooltip": 0, 54 | "id": null, 55 | "links": [], 56 | "panels": [ 57 | { 58 | "cacheTimeout": null, 59 | "datasource": "${DS_PROMETHEUS}", 60 | "gridPos": { 61 | "h": 3, 62 | "w": 24, 63 | "x": 0, 64 | "y": 0 65 | }, 66 | "id": 11, 67 | "links": [], 68 | "options": { 69 | "colorMode": "value", 70 | "fieldOptions": { 71 | "calcs": [ 72 | "lastNotNull" 73 | ], 74 | "defaults": { 75 | "mappings": [], 76 | "max": 1, 77 | "min": 1, 78 | "thresholds": { 79 | "mode": "absolute", 80 | "steps": [ 81 | { 82 | "color": "rgb(33, 33, 33)", 83 | "value": null 84 | } 85 | ] 86 | } 87 | }, 88 | "overrides": [], 89 | "values": false 90 | }, 91 | "graphMode": "area", 92 | "justifyMode": "auto", 93 | "orientation": "vertical" 94 | }, 95 | "pluginVersion": "6.6.0", 96 | "repeat": null, 97 | "targets": [ 98 | { 99 | "expr": "freebox_lan_reachable == 1", 100 | "instant": true, 101 | "legendFormat": "{{name}}", 102 | "refId": "A" 103 | } 104 | ], 105 | "timeFrom": null, 106 | "timeShift": null, 107 | "title": "Host on LAN", 108 | "transparent": true, 109 | "type": "stat" 110 | }, 111 | { 112 | "aliasColors": {}, 113 | "bars": false, 114 | "dashLength": 10, 115 | "dashes": false, 116 | "datasource": "${DS_PROMETHEUS}", 117 | "fill": 1, 118 | "fillGradient": 0, 119 | "gridPos": { 120 | "h": 8, 121 | "w": 24, 122 | "x": 0, 123 | "y": 3 124 | }, 125 | "hiddenSeries": false, 126 | "id": 2, 127 | "legend": { 128 | "alignAsTable": true, 129 | "avg": true, 130 | "current": true, 131 | "hideEmpty": false, 132 | "max": true, 133 | "min": true, 134 | "show": true, 135 | "total": false, 136 | "values": true 137 | }, 138 | "lines": true, 139 | "linewidth": 2, 140 | "links": [], 141 | "nullPointMode": "connected", 142 | "options": { 143 | "dataLinks": [] 144 | }, 145 | "percentage": false, 146 | "pointradius": 2, 147 | "points": false, 148 | "renderer": "flot", 149 | "seriesOverrides": [ 150 | { 151 | "alias": "/.*Trans.*/", 152 | "transform": "negative-Y" 153 | } 154 | ], 155 | "spaceLength": 10, 156 | "stack": false, 157 | "steppedLine": false, 158 | "targets": [ 159 | { 160 | "expr": "irate(freebox_net_down_bytes[5m])", 161 | "format": "time_series", 162 | "intervalFactor": 2, 163 | "legendFormat": "Download Usage", 164 | "refId": "A" 165 | }, 166 | { 167 | "expr": "freebox_net_bw_down_bytes / 10", 168 | "legendFormat": "Download Bandwidth", 169 | "refId": "B" 170 | } 171 | ], 172 | "thresholds": [], 173 | "timeFrom": null, 174 | "timeRegions": [], 175 | "timeShift": null, 176 | "title": "Network Traffic Download (bytes/sec)", 177 | "tooltip": { 178 | "shared": true, 179 | "sort": 0, 180 | "value_type": "individual" 181 | }, 182 | "type": "graph", 183 | "xaxis": { 184 | "buckets": null, 185 | "mode": "time", 186 | "name": null, 187 | "show": true, 188 | "values": [] 189 | }, 190 | "yaxes": [ 191 | { 192 | "decimals": null, 193 | "format": "Bps", 194 | "label": "Bytes out (-) / in (+)", 195 | "logBase": 1, 196 | "max": null, 197 | "min": "0", 198 | "show": true 199 | }, 200 | { 201 | "format": "short", 202 | "label": null, 203 | "logBase": 1, 204 | "max": null, 205 | "min": null, 206 | "show": false 207 | } 208 | ], 209 | "yaxis": { 210 | "align": false, 211 | "alignLevel": null 212 | } 213 | }, 214 | { 215 | "aliasColors": {}, 216 | "bars": false, 217 | "dashLength": 10, 218 | "dashes": false, 219 | "datasource": "${DS_PROMETHEUS}", 220 | "fill": 1, 221 | "fillGradient": 0, 222 | "gridPos": { 223 | "h": 8, 224 | "w": 24, 225 | "x": 0, 226 | "y": 11 227 | }, 228 | "hiddenSeries": false, 229 | "id": 13, 230 | "legend": { 231 | "alignAsTable": true, 232 | "avg": true, 233 | "current": true, 234 | "max": true, 235 | "min": true, 236 | "show": true, 237 | "total": false, 238 | "values": true 239 | }, 240 | "lines": true, 241 | "linewidth": 2, 242 | "links": [], 243 | "nullPointMode": "null", 244 | "options": { 245 | "dataLinks": [] 246 | }, 247 | "percentage": false, 248 | "pointradius": 2, 249 | "points": false, 250 | "renderer": "flot", 251 | "seriesOverrides": [ 252 | { 253 | "alias": "/.*Trans.*/", 254 | "transform": "negative-Y" 255 | } 256 | ], 257 | "spaceLength": 10, 258 | "stack": false, 259 | "steppedLine": false, 260 | "targets": [ 261 | { 262 | "expr": "irate(freebox_net_up_bytes[5m])", 263 | "format": "time_series", 264 | "intervalFactor": 2, 265 | "legendFormat": "Upload Usage", 266 | "refId": "A" 267 | }, 268 | { 269 | "expr": "freebox_net_bw_up_bytes / 10", 270 | "legendFormat": "Upload Bandwidth", 271 | "refId": "B" 272 | } 273 | ], 274 | "thresholds": [], 275 | "timeFrom": null, 276 | "timeRegions": [], 277 | "timeShift": null, 278 | "title": "Network Traffic Upload (bytes/sec)", 279 | "tooltip": { 280 | "shared": true, 281 | "sort": 0, 282 | "value_type": "individual" 283 | }, 284 | "type": "graph", 285 | "xaxis": { 286 | "buckets": null, 287 | "mode": "time", 288 | "name": null, 289 | "show": true, 290 | "values": [] 291 | }, 292 | "yaxes": [ 293 | { 294 | "decimals": null, 295 | "format": "Bps", 296 | "label": "Bytes out (-) / in (+)", 297 | "logBase": 1, 298 | "max": null, 299 | "min": "0", 300 | "show": true 301 | }, 302 | { 303 | "format": "short", 304 | "label": null, 305 | "logBase": 1, 306 | "max": null, 307 | "min": null, 308 | "show": false 309 | } 310 | ], 311 | "yaxis": { 312 | "align": false, 313 | "alignLevel": null 314 | } 315 | }, 316 | { 317 | "aliasColors": {}, 318 | "bars": false, 319 | "dashLength": 10, 320 | "dashes": false, 321 | "datasource": "${DS_PROMETHEUS}", 322 | "description": "", 323 | "fill": 1, 324 | "fillGradient": 0, 325 | "gridPos": { 326 | "h": 11, 327 | "w": 12, 328 | "x": 0, 329 | "y": 19 330 | }, 331 | "hiddenSeries": false, 332 | "id": 9, 333 | "legend": { 334 | "avg": false, 335 | "current": false, 336 | "max": false, 337 | "min": false, 338 | "show": true, 339 | "total": false, 340 | "values": false 341 | }, 342 | "lines": true, 343 | "linewidth": 1, 344 | "links": [], 345 | "nullPointMode": "null", 346 | "options": { 347 | "dataLinks": [] 348 | }, 349 | "percentage": false, 350 | "pluginVersion": "6.6.0", 351 | "pointradius": 2, 352 | "points": false, 353 | "renderer": "flot", 354 | "seriesOverrides": [], 355 | "spaceLength": 10, 356 | "stack": false, 357 | "steppedLine": false, 358 | "targets": [ 359 | { 360 | "expr": "freebox_system_fan_rpm", 361 | "format": "time_series", 362 | "intervalFactor": 1, 363 | "legendFormat": "{{name}}", 364 | "refId": "A" 365 | } 366 | ], 367 | "thresholds": [], 368 | "timeFrom": null, 369 | "timeRegions": [], 370 | "timeShift": null, 371 | "title": "Fan Speed", 372 | "tooltip": { 373 | "shared": true, 374 | "sort": 0, 375 | "value_type": "individual" 376 | }, 377 | "type": "graph", 378 | "xaxis": { 379 | "buckets": null, 380 | "mode": "time", 381 | "name": null, 382 | "show": true, 383 | "values": [] 384 | }, 385 | "yaxes": [ 386 | { 387 | "decimals": 3, 388 | "format": "short", 389 | "label": null, 390 | "logBase": 1, 391 | "max": null, 392 | "min": null, 393 | "show": true 394 | }, 395 | { 396 | "format": "short", 397 | "label": null, 398 | "logBase": 1, 399 | "max": null, 400 | "min": null, 401 | "show": true 402 | } 403 | ], 404 | "yaxis": { 405 | "align": false, 406 | "alignLevel": null 407 | } 408 | }, 409 | { 410 | "aliasColors": {}, 411 | "bars": false, 412 | "dashLength": 10, 413 | "dashes": false, 414 | "datasource": "${DS_PROMETHEUS}", 415 | "fill": 1, 416 | "fillGradient": 0, 417 | "gridPos": { 418 | "h": 11, 419 | "w": 12, 420 | "x": 12, 421 | "y": 19 422 | }, 423 | "hiddenSeries": false, 424 | "id": 6, 425 | "legend": { 426 | "alignAsTable": true, 427 | "avg": true, 428 | "current": true, 429 | "max": true, 430 | "min": true, 431 | "show": true, 432 | "total": false, 433 | "values": true 434 | }, 435 | "lines": true, 436 | "linewidth": 1, 437 | "links": [], 438 | "nullPointMode": "null", 439 | "options": { 440 | "dataLinks": [] 441 | }, 442 | "percentage": false, 443 | "pointradius": 2, 444 | "points": false, 445 | "renderer": "flot", 446 | "seriesOverrides": [], 447 | "spaceLength": 10, 448 | "stack": false, 449 | "steppedLine": false, 450 | "targets": [ 451 | { 452 | "expr": "freebox_system_temp_celsius", 453 | "format": "time_series", 454 | "intervalFactor": 1, 455 | "legendFormat": "{{name}}", 456 | "refId": "A" 457 | } 458 | ], 459 | "thresholds": [], 460 | "timeFrom": null, 461 | "timeRegions": [], 462 | "timeShift": null, 463 | "title": "System Temperature", 464 | "tooltip": { 465 | "shared": true, 466 | "sort": 0, 467 | "value_type": "individual" 468 | }, 469 | "type": "graph", 470 | "xaxis": { 471 | "buckets": null, 472 | "mode": "time", 473 | "name": null, 474 | "show": true, 475 | "values": [] 476 | }, 477 | "yaxes": [ 478 | { 479 | "format": "celsius", 480 | "label": null, 481 | "logBase": 1, 482 | "max": null, 483 | "min": null, 484 | "show": true 485 | }, 486 | { 487 | "format": "short", 488 | "label": null, 489 | "logBase": 1, 490 | "max": null, 491 | "min": null, 492 | "show": true 493 | } 494 | ], 495 | "yaxis": { 496 | "align": false, 497 | "alignLevel": null 498 | } 499 | } 500 | ], 501 | "refresh": "5s", 502 | "schemaVersion": 22, 503 | "style": "dark", 504 | "tags": [ 505 | "prometheus", 506 | "freebox" 507 | ], 508 | "templating": { 509 | "list": [] 510 | }, 511 | "time": { 512 | "from": "now-6h", 513 | "to": "now" 514 | }, 515 | "timepicker": { 516 | "refresh_intervals": [ 517 | "5s", 518 | "10s", 519 | "30s", 520 | "1m", 521 | "5m", 522 | "15m", 523 | "30m", 524 | "1h", 525 | "2h", 526 | "1d" 527 | ], 528 | "time_options": [ 529 | "5m", 530 | "15m", 531 | "1h", 532 | "6h", 533 | "12h", 534 | "24h", 535 | "2d", 536 | "7d", 537 | "30d" 538 | ] 539 | }, 540 | "timezone": "", 541 | "title": "Freebox", 542 | "uid": "TVsfEYmZk", 543 | "version": 14 544 | } -------------------------------------------------------------------------------- /contrib/mcanevet_freebox_grafana_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "editable": true, 16 | "gnetId": null, 17 | "graphTooltip": 0, 18 | "id": 5, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "aliasColors": {}, 23 | "bars": false, 24 | "dashLength": 10, 25 | "dashes": false, 26 | "datasource": "Prometheus", 27 | "fill": 2, 28 | "gridPos": { 29 | "h": 8, 30 | "w": 12, 31 | "x": 0, 32 | "y": 0 33 | }, 34 | "id": 2, 35 | "legend": { 36 | "alignAsTable": true, 37 | "avg": true, 38 | "current": true, 39 | "max": true, 40 | "min": true, 41 | "show": true, 42 | "total": false, 43 | "values": true 44 | }, 45 | "lines": true, 46 | "linewidth": 1, 47 | "links": [], 48 | "nullPointMode": "null", 49 | "percentage": false, 50 | "pointradius": 2, 51 | "points": false, 52 | "renderer": "flot", 53 | "seriesOverrides": [ 54 | { 55 | "alias": "/.*Trans.*/", 56 | "transform": "negative-Y" 57 | } 58 | ], 59 | "spaceLength": 10, 60 | "stack": false, 61 | "steppedLine": false, 62 | "targets": [ 63 | { 64 | "expr": "irate(freebox_net_down_bytes[5m])", 65 | "format": "time_series", 66 | "intervalFactor": 2, 67 | "legendFormat": "Receive", 68 | "refId": "B" 69 | }, 70 | { 71 | "expr": "irate(freebox_net_up_bytes[5m])", 72 | "format": "time_series", 73 | "intervalFactor": 2, 74 | "legendFormat": "Transmit", 75 | "refId": "A" 76 | } 77 | ], 78 | "thresholds": [], 79 | "timeFrom": null, 80 | "timeRegions": [], 81 | "timeShift": null, 82 | "title": "Network Traffic by bytes", 83 | "tooltip": { 84 | "shared": true, 85 | "sort": 0, 86 | "value_type": "individual" 87 | }, 88 | "type": "graph", 89 | "xaxis": { 90 | "buckets": null, 91 | "mode": "time", 92 | "name": null, 93 | "show": true, 94 | "values": [] 95 | }, 96 | "yaxes": [ 97 | { 98 | "decimals": null, 99 | "format": "Bps", 100 | "label": "Bytes out (-) / in (+)", 101 | "logBase": 1, 102 | "max": null, 103 | "min": null, 104 | "show": true 105 | }, 106 | { 107 | "format": "short", 108 | "label": null, 109 | "logBase": 1, 110 | "max": null, 111 | "min": null, 112 | "show": false 113 | } 114 | ], 115 | "yaxis": { 116 | "align": false, 117 | "alignLevel": null 118 | } 119 | }, 120 | { 121 | "aliasColors": {}, 122 | "bars": false, 123 | "dashLength": 10, 124 | "dashes": false, 125 | "fill": 1, 126 | "gridPos": { 127 | "h": 8, 128 | "w": 12, 129 | "x": 12, 130 | "y": 0 131 | }, 132 | "id": 4, 133 | "legend": { 134 | "alignAsTable": true, 135 | "avg": true, 136 | "current": true, 137 | "max": true, 138 | "min": true, 139 | "rightSide": false, 140 | "show": true, 141 | "total": false, 142 | "values": true 143 | }, 144 | "lines": true, 145 | "linewidth": 1, 146 | "links": [], 147 | "nullPointMode": "null", 148 | "percentage": false, 149 | "pointradius": 2, 150 | "points": false, 151 | "renderer": "flot", 152 | "seriesOverrides": [], 153 | "spaceLength": 10, 154 | "stack": false, 155 | "steppedLine": false, 156 | "targets": [ 157 | { 158 | "expr": "freebox_system_fan_rpm", 159 | "format": "time_series", 160 | "intervalFactor": 1, 161 | "legendFormat": "{{name}}", 162 | "refId": "A" 163 | } 164 | ], 165 | "thresholds": [], 166 | "timeFrom": null, 167 | "timeRegions": [], 168 | "timeShift": null, 169 | "title": "Fan RPM", 170 | "tooltip": { 171 | "shared": true, 172 | "sort": 0, 173 | "value_type": "individual" 174 | }, 175 | "type": "graph", 176 | "xaxis": { 177 | "buckets": null, 178 | "mode": "time", 179 | "name": null, 180 | "show": true, 181 | "values": [] 182 | }, 183 | "yaxes": [ 184 | { 185 | "decimals": null, 186 | "format": "short", 187 | "label": null, 188 | "logBase": 1, 189 | "max": null, 190 | "min": "0", 191 | "show": true 192 | }, 193 | { 194 | "format": "short", 195 | "label": null, 196 | "logBase": 1, 197 | "max": null, 198 | "min": null, 199 | "show": true 200 | } 201 | ], 202 | "yaxis": { 203 | "align": false, 204 | "alignLevel": null 205 | } 206 | }, 207 | { 208 | "aliasColors": {}, 209 | "bars": false, 210 | "dashLength": 10, 211 | "dashes": false, 212 | "fill": 1, 213 | "gridPos": { 214 | "h": 8, 215 | "w": 12, 216 | "x": 0, 217 | "y": 8 218 | }, 219 | "id": 6, 220 | "legend": { 221 | "alignAsTable": true, 222 | "avg": true, 223 | "current": true, 224 | "max": true, 225 | "min": true, 226 | "show": true, 227 | "total": false, 228 | "values": true 229 | }, 230 | "lines": true, 231 | "linewidth": 1, 232 | "links": [], 233 | "nullPointMode": "null", 234 | "percentage": false, 235 | "pointradius": 2, 236 | "points": false, 237 | "renderer": "flot", 238 | "seriesOverrides": [], 239 | "spaceLength": 10, 240 | "stack": false, 241 | "steppedLine": false, 242 | "targets": [ 243 | { 244 | "expr": "freebox_system_temp_celsius", 245 | "format": "time_series", 246 | "intervalFactor": 1, 247 | "legendFormat": "{{name}}", 248 | "refId": "A" 249 | } 250 | ], 251 | "thresholds": [], 252 | "timeFrom": null, 253 | "timeRegions": [], 254 | "timeShift": null, 255 | "title": "System Temperature", 256 | "tooltip": { 257 | "shared": true, 258 | "sort": 0, 259 | "value_type": "individual" 260 | }, 261 | "type": "graph", 262 | "xaxis": { 263 | "buckets": null, 264 | "mode": "time", 265 | "name": null, 266 | "show": true, 267 | "values": [] 268 | }, 269 | "yaxes": [ 270 | { 271 | "format": "celsius", 272 | "label": null, 273 | "logBase": 1, 274 | "max": null, 275 | "min": null, 276 | "show": true 277 | }, 278 | { 279 | "format": "short", 280 | "label": null, 281 | "logBase": 1, 282 | "max": null, 283 | "min": null, 284 | "show": true 285 | } 286 | ], 287 | "yaxis": { 288 | "align": false, 289 | "alignLevel": null 290 | } 291 | }, 292 | { 293 | "cacheTimeout": null, 294 | "colorBackground": false, 295 | "colorValue": false, 296 | "colors": [ 297 | "#299c46", 298 | "rgba(237, 129, 40, 0.89)", 299 | "#d44a3a" 300 | ], 301 | "format": "none", 302 | "gauge": { 303 | "maxValue": 100, 304 | "minValue": 0, 305 | "show": false, 306 | "thresholdLabels": false, 307 | "thresholdMarkers": true 308 | }, 309 | "gridPos": { 310 | "h": 8, 311 | "w": 12, 312 | "x": 12, 313 | "y": 8 314 | }, 315 | "id": 8, 316 | "interval": null, 317 | "links": [], 318 | "mappingType": 1, 319 | "mappingTypes": [ 320 | { 321 | "name": "value to text", 322 | "value": 1 323 | }, 324 | { 325 | "name": "range to text", 326 | "value": 2 327 | } 328 | ], 329 | "maxDataPoints": 100, 330 | "nullPointMode": "connected", 331 | "nullText": null, 332 | "postfix": "", 333 | "postfixFontSize": "50%", 334 | "prefix": "", 335 | "prefixFontSize": "50%", 336 | "rangeMaps": [ 337 | { 338 | "from": "null", 339 | "text": "N/A", 340 | "to": "null" 341 | } 342 | ], 343 | "sparkline": { 344 | "fillColor": "rgba(31, 118, 189, 0.18)", 345 | "full": false, 346 | "lineColor": "rgb(31, 120, 193)", 347 | "show": true 348 | }, 349 | "tableColumn": "", 350 | "targets": [ 351 | { 352 | "expr": "sum(freebox_lan_reachable)", 353 | "format": "time_series", 354 | "intervalFactor": 1, 355 | "legendFormat": "", 356 | "refId": "A" 357 | } 358 | ], 359 | "thresholds": "", 360 | "timeFrom": null, 361 | "timeShift": null, 362 | "title": "Hosts on LAN", 363 | "type": "singlestat", 364 | "valueFontSize": "80%", 365 | "valueMaps": [ 366 | { 367 | "op": "=", 368 | "text": "N/A", 369 | "value": "null" 370 | } 371 | ], 372 | "valueName": "current" 373 | } 374 | ], 375 | "refresh": "30s", 376 | "schemaVersion": 18, 377 | "style": "dark", 378 | "tags": [ 379 | "prometheus", 380 | "freebox" 381 | ], 382 | "templating": { 383 | "list": [] 384 | }, 385 | "time": { 386 | "from": "now-6h", 387 | "to": "now" 388 | }, 389 | "timepicker": { 390 | "refresh_intervals": [ 391 | "5s", 392 | "10s", 393 | "30s", 394 | "1m", 395 | "5m", 396 | "15m", 397 | "30m", 398 | "1h", 399 | "2h", 400 | "1d" 401 | ], 402 | "time_options": [ 403 | "5m", 404 | "15m", 405 | "1h", 406 | "6h", 407 | "12h", 408 | "24h", 409 | "2d", 410 | "7d", 411 | "30d" 412 | ] 413 | }, 414 | "timezone": "", 415 | "title": "Freebox", 416 | "uid": "TVsfEYmZk", 417 | "version": 14 418 | } 419 | -------------------------------------------------------------------------------- /gauges.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | // XXX: see https://dev.freebox.fr/sdk/os/ for API documentation 10 | // XXX: see https://prometheus.io/docs/practices/naming/ for metric names 11 | 12 | // connectionXdsl 13 | connectionXdslStatusUptimeGauges = promauto.NewGaugeVec(prometheus.GaugeOpts{ 14 | Name: "freebox_connection_xdsl_status_uptime_seconds_total", 15 | }, 16 | []string{ 17 | "status", 18 | "protocol", 19 | "modulation", 20 | }, 21 | ) 22 | 23 | connectionXdslDownAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{ 24 | Name: "freebox_connection_xdsl_down_attn_decibels", 25 | }) 26 | connectionXdslUpAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{ 27 | Name: "freebox_connection_xdsl_up_attn_decibels", 28 | }) 29 | connectionXdslDownSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{ 30 | Name: "freebox_connection_xdsl_down_snr_decibels", 31 | }) 32 | connectionXdslUpSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{ 33 | Name: "freebox_connection_xdsl_up_snr_decibels", 34 | }) 35 | 36 | connectionXdslErrorGauges = promauto.NewGaugeVec( 37 | prometheus.GaugeOpts{ 38 | Name: "freebox_connection_xdsl_errors_total", 39 | Help: "Error counts", 40 | }, 41 | []string{ 42 | "direction", // up|down 43 | "name", // crc|es|fec|hec 44 | }, 45 | ) 46 | 47 | connectionXdslGinpGauges = promauto.NewGaugeVec( 48 | prometheus.GaugeOpts{ 49 | Name: "freebox_connection_xdsl_ginp", 50 | }, 51 | []string{ 52 | "direction", // up|down 53 | "name", // enabled|rtx_(tx|c|uc) 54 | }, 55 | ) 56 | 57 | connectionXdslNitroGauges = promauto.NewGaugeVec( 58 | prometheus.GaugeOpts{ 59 | Name: "freebox_connection_xdsl_nitro", 60 | }, 61 | []string{ 62 | "direction", // up|down 63 | }, 64 | ) 65 | 66 | // RRD dsl [unstable] 67 | rateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ 68 | Name: "freebox_dsl_up_bytes", 69 | Help: "Available upload bandwidth (in byte/s)", 70 | }) 71 | rateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ 72 | Name: "freebox_dsl_down_bytes", 73 | Help: "Available download bandwidth (in byte/s)", 74 | }) 75 | snrUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ 76 | Name: "freebox_dsl_snr_up_decibel", 77 | Help: "Upload signal/noise ratio (in 1/10 dB)", 78 | }) 79 | snrDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ 80 | Name: "freebox_dsl_snr_down_decibel", 81 | Help: "Download signal/noise ratio (in 1/10 dB)", 82 | }) 83 | 84 | // freeplug 85 | freeplugRxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ 86 | Name: "freebox_freeplug_rx_rate_bits", 87 | Help: "rx rate (from the freeplugs to the \"cco\" freeplug) (in bits/s) -1 if not available", 88 | }, 89 | []string{ 90 | "id", 91 | }, 92 | ) 93 | freeplugTxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ 94 | Name: "freebox_freeplug_tx_rate_bits", 95 | Help: "tx rate (from the \"cco\" freeplug to the freeplugs) (in bits/s) -1 if not available", 96 | }, 97 | []string{ 98 | "id", 99 | }, 100 | ) 101 | freeplugHasNetworkGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ 102 | Name: "freebox_freeplug_has_network", 103 | Help: "is connected to the network", 104 | }, 105 | []string{ 106 | "id", 107 | }, 108 | ) 109 | 110 | // RRD Net [unstable] 111 | bwUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ 112 | Name: "freebox_net_bw_up_bytes", 113 | Help: "Upload available bandwidth (in byte/s)", 114 | }) 115 | bwDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ 116 | Name: "freebox_net_bw_down_bytes", 117 | Help: "Download available bandwidth (in byte/s)", 118 | }) 119 | netRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ 120 | Name: "freebox_net_up_bytes", 121 | Help: "Upload rate (in byte/s)", 122 | }) 123 | netRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ 124 | Name: "freebox_net_down_bytes", 125 | Help: "Download rate (in byte/s)", 126 | }) 127 | vpnRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ 128 | Name: "freebox_net_vpn_up_bytes", 129 | Help: "Vpn client upload rate (in byte/s)", 130 | }) 131 | vpnRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ 132 | Name: "freebox_net_vpn_down_bytes", 133 | Help: "Vpn client download rate (in byte/s)", 134 | }) 135 | 136 | // Lan 137 | lanReachableGauges = promauto.NewGaugeVec( 138 | prometheus.GaugeOpts{ 139 | Name: "freebox_lan_reachable", 140 | Help: "Hosts reachable on LAN", 141 | }, 142 | []string{ 143 | "name", // hostname 144 | "vendor", 145 | "ip", 146 | }, 147 | ) 148 | 149 | systemTempGauges = promauto.NewGaugeVec( 150 | prometheus.GaugeOpts{ 151 | Name: "freebox_system_temp_celsius", 152 | Help: "Temperature sensors reported by system (in °C)", 153 | }, 154 | []string{ 155 | "name", 156 | }, 157 | ) 158 | 159 | systemFanGauges = promauto.NewGaugeVec( 160 | prometheus.GaugeOpts{ 161 | Name: "freebox_system_fan_rpm", 162 | Help: "Fan speed reported by system (in RPM)", 163 | }, 164 | []string{ 165 | "name", 166 | }, 167 | ) 168 | 169 | systemUptimeGauges = promauto.NewGaugeVec( 170 | prometheus.GaugeOpts{ 171 | Name: "freebox_system_uptime_seconds_total", 172 | }, 173 | []string{ 174 | "firmware_version", 175 | }, 176 | ) 177 | 178 | // wifi 179 | wifiLabels = []string{ 180 | "access_point", 181 | "hostname", 182 | "state", 183 | } 184 | 185 | wifiSignalGauges = promauto.NewGaugeVec( 186 | prometheus.GaugeOpts{ 187 | Name: "freebox_wifi_signal_attenuation_db", 188 | Help: "Wifi signal attenuation in decibel", 189 | }, 190 | wifiLabels, 191 | ) 192 | 193 | wifiInactiveGauges = promauto.NewGaugeVec( 194 | prometheus.GaugeOpts{ 195 | Name: "freebox_wifi_inactive_duration_seconds", 196 | Help: "Wifi inactive duration in seconds", 197 | }, 198 | wifiLabels, 199 | ) 200 | 201 | wifiConnectionDurationGauges = promauto.NewGaugeVec( 202 | prometheus.GaugeOpts{ 203 | Name: "freebox_wifi_connection_duration_seconds", 204 | Help: "Wifi connection duration in seconds", 205 | }, 206 | wifiLabels, 207 | ) 208 | 209 | wifiRXBytesGauges = promauto.NewGaugeVec( 210 | prometheus.GaugeOpts{ 211 | Name: "freebox_wifi_rx_bytes", 212 | Help: "Wifi received data (from station to Freebox) in bytes", 213 | }, 214 | wifiLabels, 215 | ) 216 | 217 | wifiTXBytesGauges = promauto.NewGaugeVec( 218 | prometheus.GaugeOpts{ 219 | Name: "freebox_wifi_tx_bytes", 220 | Help: "Wifi transmitted data (from Freebox to station) in bytes", 221 | }, 222 | wifiLabels, 223 | ) 224 | 225 | wifiRXRateGauges = promauto.NewGaugeVec( 226 | prometheus.GaugeOpts{ 227 | Name: "freebox_wifi_rx_rate", 228 | Help: "Wifi reception data rate (from station to Freebox) in bytes/seconds", 229 | }, 230 | wifiLabels, 231 | ) 232 | 233 | wifiTXRateGauges = promauto.NewGaugeVec( 234 | prometheus.GaugeOpts{ 235 | Name: "freebox_wifi_tx_rate", 236 | Help: "Wifi transmission data rate (from Freebox to station) in bytes/seconds", 237 | }, 238 | wifiLabels, 239 | ) 240 | 241 | // vpn server connections list [unstable] 242 | vpnServerConnectionsList = promauto.NewGaugeVec( 243 | prometheus.GaugeOpts{ 244 | Name: "vpn_server_connections_list", 245 | Help: "VPN server connections list", 246 | }, 247 | []string{ 248 | "user", 249 | "vpn", 250 | "src_ip", 251 | "local_ip", 252 | "name", // rx_bytes|tx_bytes 253 | }, 254 | ) 255 | ) 256 | -------------------------------------------------------------------------------- /getters.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | var ( 16 | apiErrors = map[string]error{ 17 | "invalid_token": errors.New("The app token you are trying to use is invalid or has been revoked"), 18 | "insufficient_rights": errors.New("Your app permissions does not allow accessing this API"), 19 | "denied_from_external_ip": errors.New("You are trying to get an app_token from a remote IP"), 20 | "invalid_request": errors.New("Your request is invalid"), 21 | "ratelimited": errors.New("Too many auth error have been made from your IP"), 22 | "new_apps_denied": errors.New("New application token request has been disabled"), 23 | "apps_denied": errors.New("API access from apps has been disabled"), 24 | "internal_error": errors.New("Internal error"), 25 | "db_error": errors.New("Oops, the database you are trying to access doesn't seem to exist"), 26 | "nodev": errors.New("Invalid interface"), 27 | } 28 | ) 29 | 30 | func (r *rrd) status() error { 31 | if apiErrors[r.ErrorCode] == nil { 32 | return errors.New("RRD: The API returns an unknown error_code: " + r.ErrorCode) 33 | } 34 | return apiErrors[r.ErrorCode] 35 | } 36 | 37 | func (l *lan) status() error { 38 | if apiErrors[l.ErrorCode] == nil { 39 | return errors.New("LAN: The API returns an unknown error_code: " + l.ErrorCode) 40 | } 41 | return apiErrors[l.ErrorCode] 42 | } 43 | 44 | func setFreeboxToken(authInf *authInfo, xSessionToken *string) (string, error) { 45 | token := os.Getenv("FREEBOX_TOKEN") 46 | 47 | if token == "" { 48 | var err error 49 | *xSessionToken, err = getToken(authInf, xSessionToken) 50 | if err != nil { 51 | return "", err 52 | } 53 | token = *xSessionToken 54 | } 55 | 56 | if *xSessionToken == "" { 57 | var err error 58 | *xSessionToken, err = getSessToken(token, authInf, xSessionToken) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | token = *xSessionToken 63 | } 64 | 65 | return token, nil 66 | 67 | } 68 | 69 | func newPostRequest() *postRequest { 70 | return &postRequest{ 71 | method: "POST", 72 | url: mafreebox + "api/v4/rrd/", 73 | header: "X-Fbx-App-Auth", 74 | } 75 | } 76 | 77 | func getConnectionXdsl(authInf *authInfo, pr *postRequest, xSessionToken *string) (connectionXdsl, error) { 78 | client := http.Client{} 79 | req, err := http.NewRequest(pr.method, pr.url, nil) 80 | if err != nil { 81 | return connectionXdsl{}, err 82 | } 83 | req.Header.Add(pr.header, *xSessionToken) 84 | resp, err := client.Do(req) 85 | if err != nil { 86 | return connectionXdsl{}, err 87 | } 88 | if resp.StatusCode == 404 { 89 | return connectionXdsl{}, errors.New(resp.Status) 90 | } 91 | body, err := ioutil.ReadAll(resp.Body) 92 | if err != nil { 93 | return connectionXdsl{}, err 94 | } 95 | 96 | connectionXdslResp := connectionXdsl{} 97 | err = json.Unmarshal(body, &connectionXdslResp) 98 | if err != nil { 99 | if debug { 100 | log.Println(string(body)) 101 | } 102 | return connectionXdsl{}, err 103 | } 104 | 105 | return connectionXdslResp, nil 106 | } 107 | 108 | func getDsl(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { 109 | d := &database{ 110 | DB: "dsl", 111 | Fields: []string{"rate_up", "rate_down", "snr_up", "snr_down"}, 112 | Precision: 10, 113 | DateStart: int(time.Now().Unix() - 10), 114 | } 115 | 116 | freeboxToken, err := setFreeboxToken(authInf, xSessionToken) 117 | if err != nil { 118 | return []int64{}, err 119 | } 120 | client := http.Client{} 121 | r, err := json.Marshal(*d) 122 | if err != nil { 123 | return []int64{}, err 124 | } 125 | buf := bytes.NewReader(r) 126 | req, err := http.NewRequest(pr.method, pr.url, buf) 127 | if err != nil { 128 | return []int64{}, err 129 | } 130 | req.Header.Add(pr.header, *xSessionToken) 131 | resp, err := client.Do(req) 132 | if err != nil { 133 | return []int64{}, err 134 | } 135 | if resp.StatusCode == 404 { 136 | return []int64{}, errors.New(resp.Status) 137 | } 138 | body, err := ioutil.ReadAll(resp.Body) 139 | if err != nil { 140 | return []int64{}, err 141 | } 142 | rrdTest := rrd{} 143 | err = json.Unmarshal(body, &rrdTest) 144 | if err != nil { 145 | if debug { 146 | log.Println(string(body)) 147 | } 148 | return []int64{}, err 149 | } 150 | 151 | if rrdTest.ErrorCode == "auth_required" { 152 | *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) 153 | if err != nil { 154 | return []int64{}, err 155 | } 156 | } 157 | 158 | if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { 159 | if rrdTest.status().Error() == "Unknown return code from the API" { 160 | fmt.Println("getDsl") 161 | } 162 | return []int64{}, rrdTest.status() 163 | } 164 | 165 | if len(rrdTest.Result.Data) == 0 { 166 | return []int64{}, nil 167 | } 168 | 169 | result := []int64{rrdTest.Result.Data[0]["rate_up"], rrdTest.Result.Data[0]["rate_down"], rrdTest.Result.Data[0]["snr_up"], rrdTest.Result.Data[0]["snr_down"]} 170 | return result, nil 171 | } 172 | 173 | func getTemp(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { 174 | d := &database{ 175 | DB: "temp", 176 | Fields: []string{"cpum", "cpub", "sw", "hdd", "fan_speed"}, 177 | Precision: 10, 178 | DateStart: int(time.Now().Unix() - 10), 179 | } 180 | 181 | freeboxToken, err := setFreeboxToken(authInf, xSessionToken) 182 | if err != nil { 183 | return []int64{}, err 184 | } 185 | 186 | client := http.Client{} 187 | r, err := json.Marshal(*d) 188 | if err != nil { 189 | return []int64{}, err 190 | } 191 | buf := bytes.NewReader(r) 192 | req, err := http.NewRequest(pr.method, fmt.Sprintf(pr.url), buf) 193 | if err != nil { 194 | return []int64{}, err 195 | } 196 | req.Header.Add(pr.header, *xSessionToken) 197 | resp, err := client.Do(req) 198 | if err != nil { 199 | return []int64{}, err 200 | } 201 | if resp.StatusCode == 404 { 202 | return []int64{}, errors.New(resp.Status) 203 | } 204 | body, err := ioutil.ReadAll(resp.Body) 205 | if err != nil { 206 | return []int64{}, err 207 | } 208 | rrdTest := rrd{} 209 | err = json.Unmarshal(body, &rrdTest) 210 | if err != nil { 211 | if debug { 212 | log.Println(string(body)) 213 | } 214 | return []int64{}, err 215 | } 216 | 217 | if rrdTest.ErrorCode == "auth_required" { 218 | *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) 219 | if err != nil { 220 | return []int64{}, err 221 | } 222 | } 223 | 224 | if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { 225 | if rrdTest.status().Error() == "Unknown return code from the API" { 226 | fmt.Println("getTemp") 227 | } 228 | return []int64{}, rrdTest.status() 229 | } 230 | 231 | if len(rrdTest.Result.Data) == 0 { 232 | return []int64{}, nil 233 | } 234 | 235 | return []int64{rrdTest.Result.Data[0]["cpum"], rrdTest.Result.Data[0]["cpub"], rrdTest.Result.Data[0]["sw"], rrdTest.Result.Data[0]["hdd"], rrdTest.Result.Data[0]["fan_speed"]}, nil 236 | } 237 | 238 | func getNet(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { 239 | d := &database{ 240 | DB: "net", 241 | Fields: []string{"bw_up", "bw_down", "rate_up", "rate_down", "vpn_rate_up", "vpn_rate_down"}, 242 | Precision: 10, 243 | DateStart: int(time.Now().Unix() - 10), 244 | } 245 | 246 | freeboxToken, err := setFreeboxToken(authInf, xSessionToken) 247 | if err != nil { 248 | return []int64{}, err 249 | } 250 | 251 | client := http.Client{} 252 | r, err := json.Marshal(*d) 253 | if err != nil { 254 | return []int64{}, err 255 | } 256 | buf := bytes.NewReader(r) 257 | req, err := http.NewRequest(pr.method, pr.url, buf) 258 | if err != nil { 259 | return []int64{}, err 260 | } 261 | req.Header.Add(pr.header, *xSessionToken) 262 | resp, err := client.Do(req) 263 | if err != nil { 264 | return []int64{}, err 265 | } 266 | if resp.StatusCode == 404 { 267 | return []int64{}, errors.New(resp.Status) 268 | } 269 | body, err := ioutil.ReadAll(resp.Body) 270 | if err != nil { 271 | return []int64{}, err 272 | } 273 | rrdTest := rrd{} 274 | err = json.Unmarshal(body, &rrdTest) 275 | if err != nil { 276 | if debug { 277 | log.Println(string(body)) 278 | } 279 | return []int64{}, err 280 | } 281 | 282 | if rrdTest.ErrorCode == "auth_required" { 283 | *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) 284 | if err != nil { 285 | return []int64{}, err 286 | } 287 | } 288 | 289 | if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { 290 | if rrdTest.status().Error() == "Unknown return code from the API" { 291 | fmt.Println("getNet") 292 | } 293 | return []int64{}, rrdTest.status() 294 | } 295 | 296 | if len(rrdTest.Result.Data) == 0 { 297 | return []int64{}, nil 298 | } 299 | 300 | return []int64{rrdTest.Result.Data[0]["bw_up"], rrdTest.Result.Data[0]["bw_down"], rrdTest.Result.Data[0]["rate_up"], rrdTest.Result.Data[0]["rate_down"], rrdTest.Result.Data[0]["vpn_rate_up"], rrdTest.Result.Data[0]["vpn_rate_down"]}, nil 301 | } 302 | 303 | func getSwitch(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { 304 | d := &database{ 305 | DB: "switch", 306 | Fields: []string{"rx_1", "tx_1", "rx_2", "tx_2", "rx_3", "tx_3", "rx_4", "tx_4"}, 307 | Precision: 10, 308 | DateStart: int(time.Now().Unix() - 10), 309 | } 310 | 311 | freeboxToken, err := setFreeboxToken(authInf, xSessionToken) 312 | if err != nil { 313 | return []int64{}, err 314 | } 315 | 316 | client := http.Client{} 317 | r, err := json.Marshal(*d) 318 | if err != nil { 319 | return []int64{}, err 320 | } 321 | buf := bytes.NewReader(r) 322 | req, err := http.NewRequest(pr.method, pr.url, buf) 323 | if err != nil { 324 | return []int64{}, err 325 | } 326 | req.Header.Add(pr.header, *xSessionToken) 327 | resp, err := client.Do(req) 328 | if err != nil { 329 | return []int64{}, err 330 | } 331 | if resp.StatusCode == 404 { 332 | return []int64{}, errors.New(resp.Status) 333 | } 334 | body, err := ioutil.ReadAll(resp.Body) 335 | if err != nil { 336 | return []int64{}, err 337 | } 338 | rrdTest := rrd{} 339 | err = json.Unmarshal(body, &rrdTest) 340 | if err != nil { 341 | if debug { 342 | log.Println(string(body)) 343 | } 344 | return []int64{}, err 345 | } 346 | 347 | if rrdTest.ErrorCode == "auth_required" { 348 | *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) 349 | if err != nil { 350 | return []int64{}, err 351 | } 352 | } 353 | 354 | if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { 355 | if rrdTest.status().Error() == "Unknown return code from the API" { 356 | fmt.Println("getSwitch") 357 | } 358 | return []int64{}, rrdTest.status() 359 | } 360 | 361 | if len(rrdTest.Result.Data) == 0 { 362 | return []int64{}, nil 363 | } 364 | 365 | return []int64{rrdTest.Result.Data[0]["rx_1"], rrdTest.Result.Data[0]["tx_1"], rrdTest.Result.Data[0]["rx_2"], rrdTest.Result.Data[0]["tx_2"], rrdTest.Result.Data[0]["rx_3"], rrdTest.Result.Data[0]["tx_3"], rrdTest.Result.Data[0]["rx_4"], rrdTest.Result.Data[0]["tx_4"]}, nil 366 | } 367 | 368 | func getLan(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]lanHost, error) { 369 | freeboxToken, err := setFreeboxToken(authInf, xSessionToken) 370 | if err != nil { 371 | return []lanHost{}, err 372 | } 373 | 374 | client := http.Client{} 375 | req, err := http.NewRequest(pr.method, pr.url, nil) 376 | if err != nil { 377 | return []lanHost{}, err 378 | } 379 | req.Header.Add(pr.header, *xSessionToken) 380 | resp, err := client.Do(req) 381 | if err != nil { 382 | return []lanHost{}, err 383 | } 384 | if resp.StatusCode == 404 { 385 | return []lanHost{}, err 386 | } 387 | 388 | body, err := ioutil.ReadAll(resp.Body) 389 | if err != nil { 390 | return []lanHost{}, err 391 | } 392 | 393 | lanResp := lan{} 394 | err = json.Unmarshal(body, &lanResp) 395 | if err != nil { 396 | if debug { 397 | log.Println(string(body)) 398 | } 399 | return []lanHost{}, err 400 | } 401 | 402 | if lanResp.ErrorCode == "auth_required" { 403 | *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) 404 | if err != nil { 405 | return []lanHost{}, err 406 | } 407 | } 408 | 409 | if lanResp.ErrorCode != "" && lanResp.ErrorCode != "auth_required" { 410 | return []lanHost{}, lanResp.status() 411 | } 412 | 413 | return lanResp.Result, nil 414 | } 415 | 416 | func getFreeplug(authInf *authInfo, pr *postRequest, xSessionToken *string) (freeplug, error) { 417 | if _, err := setFreeboxToken(authInf, xSessionToken); err != nil { 418 | return freeplug{}, err 419 | } 420 | 421 | client := http.Client{} 422 | req, err := http.NewRequest(pr.method, pr.url, nil) 423 | if err != nil { 424 | return freeplug{}, err 425 | } 426 | req.Header.Add(pr.header, *xSessionToken) 427 | resp, err := client.Do(req) 428 | if err != nil { 429 | return freeplug{}, err 430 | } 431 | if resp.StatusCode == 404 { 432 | return freeplug{}, errors.New(resp.Status) 433 | } 434 | body, err := ioutil.ReadAll(resp.Body) 435 | if err != nil { 436 | return freeplug{}, err 437 | } 438 | 439 | freeplugResp := freeplug{} 440 | err = json.Unmarshal(body, &freeplugResp) 441 | if err != nil { 442 | if debug { 443 | log.Println(string(body)) 444 | } 445 | return freeplug{}, err 446 | } 447 | 448 | return freeplugResp, nil 449 | } 450 | 451 | func getSystem(authInf *authInfo, pr *postRequest, xSessionToken *string) (system, error) { 452 | client := http.Client{} 453 | req, err := http.NewRequest(pr.method, pr.url, nil) 454 | if err != nil { 455 | return system{}, err 456 | } 457 | req.Header.Add(pr.header, *xSessionToken) 458 | resp, err := client.Do(req) 459 | if err != nil { 460 | return system{}, err 461 | } 462 | if resp.StatusCode == 404 { 463 | return system{}, errors.New(resp.Status) 464 | } 465 | body, err := ioutil.ReadAll(resp.Body) 466 | if err != nil { 467 | return system{}, err 468 | } 469 | 470 | systemResp := system{} 471 | err = json.Unmarshal(body, &systemResp) 472 | if err != nil { 473 | if debug { 474 | log.Println(string(body)) 475 | } 476 | return system{}, err 477 | } 478 | 479 | return systemResp, nil 480 | } 481 | 482 | func getWifi(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifi, error) { 483 | client := http.Client{} 484 | req, err := http.NewRequest(pr.method, pr.url, nil) 485 | if err != nil { 486 | return wifi{}, err 487 | } 488 | req.Header.Add(pr.header, *xSessionToken) 489 | resp, err := client.Do(req) 490 | if err != nil { 491 | return wifi{}, err 492 | } 493 | if resp.StatusCode == 404 { 494 | return wifi{}, errors.New(resp.Status) 495 | } 496 | body, err := ioutil.ReadAll(resp.Body) 497 | if err != nil { 498 | return wifi{}, err 499 | } 500 | 501 | wifiResp := wifi{} 502 | err = json.Unmarshal(body, &wifiResp) 503 | if err != nil { 504 | if debug { 505 | log.Println(string(body)) 506 | } 507 | return wifi{}, err 508 | } 509 | 510 | return wifiResp, nil 511 | } 512 | 513 | func getWifiStations(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifiStations, error) { 514 | client := http.Client{} 515 | req, err := http.NewRequest(pr.method, pr.url, nil) 516 | if err != nil { 517 | return wifiStations{}, err 518 | } 519 | req.Header.Add(pr.header, *xSessionToken) 520 | resp, err := client.Do(req) 521 | if err != nil { 522 | return wifiStations{}, err 523 | } 524 | if resp.StatusCode == 404 { 525 | return wifiStations{}, errors.New(resp.Status) 526 | } 527 | body, err := ioutil.ReadAll(resp.Body) 528 | if err != nil { 529 | return wifiStations{}, err 530 | } 531 | 532 | wifiStationResp := wifiStations{} 533 | err = json.Unmarshal(body, &wifiStationResp) 534 | if err != nil { 535 | if debug { 536 | log.Println(string(body)) 537 | } 538 | return wifiStations{}, err 539 | } 540 | 541 | return wifiStationResp, nil 542 | } 543 | 544 | func getVpnServer(authInf *authInfo, pr *postRequest, xSessionToken *string) (vpnServer, error) { 545 | client := http.Client{} 546 | req, err := http.NewRequest(pr.method, pr.url, nil) 547 | if err != nil { 548 | return vpnServer{}, err 549 | } 550 | req.Header.Add(pr.header, *xSessionToken) 551 | resp, err := client.Do(req) 552 | if err != nil { 553 | return vpnServer{}, err 554 | } 555 | if resp.StatusCode == 404 { 556 | return vpnServer{}, errors.New(resp.Status) 557 | } 558 | body, err := ioutil.ReadAll(resp.Body) 559 | if err != nil { 560 | return vpnServer{}, err 561 | } 562 | 563 | vpnServerResp := vpnServer{} 564 | err = json.Unmarshal(body, &vpnServerResp) 565 | if err != nil { 566 | if debug { 567 | log.Println(string(body)) 568 | } 569 | return vpnServer{}, err 570 | } 571 | 572 | return vpnServerResp, nil 573 | } 574 | -------------------------------------------------------------------------------- /getters_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestSetFreeboxToken(t *testing.T) { 16 | 17 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | switch r.RequestURI { 19 | case "/login": 20 | myChall := &challenge{ 21 | Success: true, 22 | } 23 | myChall.Result.Challenge = "foobar" 24 | result, _ := json.Marshal(myChall) 25 | fmt.Fprintln(w, string(result)) 26 | case "/session": 27 | myToken := sessionToken{ 28 | Success: true, 29 | } 30 | myToken.Result.SessionToken = "foobar" 31 | result, _ := json.Marshal(myToken) 32 | fmt.Fprintln(w, string(result)) 33 | case "/granted/": 34 | myTrack := track{ 35 | Success: true, 36 | } 37 | myTrack.Result.TrackID = 101 38 | result, _ := json.Marshal(myTrack) 39 | fmt.Fprintln(w, string(result)) 40 | case "/granted/101": 41 | myGrant := grant{ 42 | Success: true, 43 | } 44 | myGrant.Result.Status = "granted" 45 | result, _ := json.Marshal(myGrant) 46 | fmt.Fprintln(w, string(result)) 47 | default: 48 | fmt.Fprintln(w, http.StatusNotFound) 49 | } 50 | })) 51 | defer ts.Close() 52 | 53 | ai := &authInfo{} 54 | ai.myStore.location = "/tmp/token" 55 | ai.myAPI.login = ts.URL + "/login" 56 | ai.myAPI.loginSession = ts.URL + "/session" 57 | ai.myAPI.authz = ts.URL + "/granted/" 58 | ai.myReader = bufio.NewReader(strings.NewReader("\n")) 59 | 60 | var mySessionToken string 61 | 62 | token, err := setFreeboxToken(ai, &mySessionToken) 63 | if err != nil { 64 | t.Error("Expected no err, but got", err) 65 | } 66 | defer os.Remove(ai.myStore.location) 67 | 68 | if token != "foobar" { 69 | t.Error("Expected foobar, but got", token) 70 | } 71 | 72 | os.Setenv("FREEBOX_TOKEN", "barfoo") 73 | defer os.Unsetenv("FREEBOX_TOKEN") 74 | 75 | token, err = setFreeboxToken(ai, &mySessionToken) 76 | if err != nil { 77 | t.Error("Expected no err, but got", err) 78 | } 79 | 80 | if token != "barfoo" { 81 | t.Error("Expected barfoo, but got", token) 82 | } 83 | } 84 | 85 | func TestGetDsl(t *testing.T) { 86 | os.Setenv("FREEBOX_TOKEN", "IOI") 87 | defer os.Unsetenv("FREEBOX_TOKEN") 88 | 89 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | switch r.RequestURI { 91 | case "/good": 92 | myRRD := rrd{ 93 | Success: true, 94 | } 95 | myRRD.Result.Data = []map[string]int64{ 96 | { 97 | "rate_up": 12, 98 | "rate_down": 34, 99 | "snr_up": 56, 100 | "snr_down": 78, 101 | }, 102 | } 103 | result, _ := json.Marshal(myRRD) 104 | fmt.Fprintln(w, string(result)) 105 | case "/error": 106 | myRRD := rrd{ 107 | Success: true, 108 | ErrorCode: "insufficient_rights", 109 | } 110 | result, _ := json.Marshal(myRRD) 111 | fmt.Fprintln(w, string(result)) 112 | case "/null": 113 | myRRD := rrd{ 114 | Success: true, 115 | } 116 | result, _ := json.Marshal(myRRD) 117 | fmt.Fprintln(w, string(result)) 118 | } 119 | })) 120 | defer ts.Close() 121 | 122 | goodPR := &postRequest{ 123 | method: "POST", 124 | header: "X-Fbx-App-Auth", 125 | url: ts.URL + "/good", 126 | } 127 | 128 | errorPR := &postRequest{ 129 | method: "POST", 130 | header: "X-Fbx-App-Auth", 131 | url: ts.URL + "/error", 132 | } 133 | 134 | nullPR := &postRequest{ 135 | method: "POST", 136 | header: "X-Fbx-App-Auth", 137 | url: ts.URL + "/null", 138 | } 139 | 140 | ai := &authInfo{} 141 | mySessionToken := "foobar" 142 | 143 | getDslResult, err := getDsl(ai, goodPR, &mySessionToken) 144 | if err != nil { 145 | t.Error("Expected no err, but got", err) 146 | } 147 | 148 | if getDslResult[0] != 12 || getDslResult[1] != 34 || getDslResult[2] != 56 || getDslResult[3] != 78 { 149 | t.Errorf("Expected 12 34 56 78, but got %v %v %v %v\n", getDslResult[0], getDslResult[1], getDslResult[2], getDslResult[3]) 150 | } 151 | 152 | getDslResult, err = getDsl(ai, errorPR, &mySessionToken) 153 | if err.Error() != "Your app permissions does not allow accessing this API" { 154 | t.Error("Expected Your app permissions does not allow accessing this API, but go", err) 155 | } 156 | 157 | if len(getDslResult) != 0 { 158 | t.Error("Expected 0, but got", len(getDslResult)) 159 | } 160 | 161 | getDslResult, err = getDsl(ai, nullPR, &mySessionToken) 162 | if err != nil { 163 | t.Error("Expected no err, but got", err) 164 | } 165 | 166 | if len(getDslResult) != 0 { 167 | t.Error("Expected 0, but got", len(getDslResult)) 168 | } 169 | 170 | } 171 | 172 | func TestGetTemp(t *testing.T) { 173 | os.Setenv("FREEBOX_TOKEN", "IOI") 174 | defer os.Unsetenv("FREEBOX_TOKEN") 175 | 176 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 177 | switch r.RequestURI { 178 | case "/good": 179 | myRRD := rrd{ 180 | Success: true, 181 | } 182 | myRRD.Result.Data = []map[string]int64{ 183 | { 184 | "cpum": 01, 185 | "cpub": 02, 186 | "sw": 03, 187 | "hdd": 04, 188 | "fan_speed": 05, 189 | }, 190 | } 191 | result, _ := json.Marshal(myRRD) 192 | fmt.Fprintln(w, string(result)) 193 | case "/error": 194 | myRRD := rrd{ 195 | Success: true, 196 | ErrorCode: "denied_from_external_ip", 197 | } 198 | result, _ := json.Marshal(myRRD) 199 | fmt.Fprintln(w, string(result)) 200 | case "/null": 201 | myRRD := rrd{ 202 | Success: true, 203 | } 204 | result, _ := json.Marshal(myRRD) 205 | fmt.Fprintln(w, string(result)) 206 | } 207 | })) 208 | defer ts.Close() 209 | 210 | goodPR := &postRequest{ 211 | method: "POST", 212 | header: "X-Fbx-App-Auth", 213 | url: ts.URL + "/good", 214 | } 215 | 216 | errorPR := &postRequest{ 217 | method: "POST", 218 | header: "X-Fbx-App-Auth", 219 | url: ts.URL + "/error", 220 | } 221 | 222 | nullPR := &postRequest{ 223 | method: "POST", 224 | header: "X-Fbx-App-Auth", 225 | url: ts.URL + "/null", 226 | } 227 | 228 | ai := &authInfo{} 229 | mySessionToken := "foobar" 230 | 231 | getTempResult, err := getTemp(ai, goodPR, &mySessionToken) 232 | if err != nil { 233 | t.Error("Expected no err, but got", err) 234 | } 235 | 236 | if getTempResult[0] != 01 || getTempResult[1] != 02 || getTempResult[2] != 03 || getTempResult[3] != 04 || getTempResult[4] != 05 { 237 | t.Errorf("Expected 01 02 03 04 05, but got %v %v %v %v %v\n", getTempResult[0], getTempResult[1], getTempResult[2], getTempResult[3], getTempResult[4]) 238 | } 239 | 240 | getTempResult, err = getTemp(ai, errorPR, &mySessionToken) 241 | if err.Error() != "You are trying to get an app_token from a remote IP" { 242 | t.Error("Expected You are trying to get an app_token from a remote IP, but go", err) 243 | } 244 | 245 | if len(getTempResult) != 0 { 246 | t.Error("Expected 0, but got", len(getTempResult)) 247 | } 248 | 249 | getTempResult, err = getTemp(ai, nullPR, &mySessionToken) 250 | if err != nil { 251 | t.Error("Expected no err, but got", err) 252 | } 253 | 254 | if len(getTempResult) != 0 { 255 | t.Error("Expected 0, but got", len(getTempResult)) 256 | } 257 | 258 | } 259 | 260 | func TestGetNet(t *testing.T) { 261 | os.Setenv("FREEBOX_TOKEN", "IOI") 262 | defer os.Unsetenv("FREEBOX_TOKEN") 263 | 264 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 265 | switch r.RequestURI { 266 | case "/good": 267 | myRRD := rrd{ 268 | Success: true, 269 | } 270 | myRRD.Result.Data = []map[string]int64{ 271 | { 272 | "bw_up": 12500000000, 273 | "bw_down": 12500000000, 274 | "rate_up": 12500000000, 275 | "rate_down": 12500000000, 276 | "vpn_rate_up": 12500000000, 277 | "vpn_rate_down": 12500000000, 278 | }, 279 | } 280 | result, _ := json.Marshal(myRRD) 281 | fmt.Fprintln(w, string(result)) 282 | case "/error": 283 | myRRD := rrd{ 284 | Success: true, 285 | ErrorCode: "new_apps_denied", 286 | } 287 | result, _ := json.Marshal(myRRD) 288 | fmt.Fprintln(w, string(result)) 289 | case "/null": 290 | myRRD := rrd{ 291 | Success: true, 292 | } 293 | result, _ := json.Marshal(myRRD) 294 | fmt.Fprintln(w, string(result)) 295 | } 296 | })) 297 | defer ts.Close() 298 | 299 | goodPR := &postRequest{ 300 | method: "POST", 301 | header: "X-Fbx-App-Auth", 302 | url: ts.URL + "/good", 303 | } 304 | 305 | errorPR := &postRequest{ 306 | method: "POST", 307 | header: "X-Fbx-App-Auth", 308 | url: ts.URL + "/error", 309 | } 310 | 311 | nullPR := &postRequest{ 312 | method: "POST", 313 | header: "X-Fbx-App-Auth", 314 | url: ts.URL + "/null", 315 | } 316 | 317 | ai := &authInfo{} 318 | mySessionToken := "foobar" 319 | 320 | getNetResult, err := getNet(ai, goodPR, &mySessionToken) 321 | if err != nil { 322 | t.Error("Expected no err, but go", err) 323 | } 324 | 325 | if getNetResult[0] != 12500000000 || getNetResult[1] != 12500000000 || getNetResult[2] != 12500000000 || getNetResult[3] != 12500000000 || getNetResult[4] != 12500000000 || getNetResult[5] != 12500000000 { 326 | t.Errorf("Expected 01 02 03 04 05 06, but got %v %v %v %v %v %v\n", getNetResult[0], getNetResult[1], getNetResult[2], getNetResult[3], getNetResult[4], getNetResult[5]) 327 | } 328 | 329 | getNetResult, err = getNet(ai, errorPR, &mySessionToken) 330 | if err.Error() != "New application token request has been disabled" { 331 | t.Error("Expected New application token request has been disabled, but got", err) 332 | } 333 | 334 | if len(getNetResult) != 0 { 335 | t.Error("Expected 0, but got", len(getNetResult)) 336 | } 337 | 338 | getNetResult, err = getNet(ai, nullPR, &mySessionToken) 339 | if err != nil { 340 | t.Error("Expected no err, but got", err) 341 | } 342 | 343 | if len(getNetResult) != 0 { 344 | t.Error("Expected 0, but got", len(getNetResult)) 345 | } 346 | 347 | } 348 | 349 | func TestGetSwitch(t *testing.T) { 350 | os.Setenv("FREEBOX_TOKEN", "IOI") 351 | defer os.Unsetenv("FREEBOX_TOKEN") 352 | 353 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 354 | switch r.RequestURI { 355 | case "/good": 356 | myRRD := rrd{ 357 | Success: true, 358 | } 359 | myRRD.Result.Data = []map[string]int64{ 360 | { 361 | "rx_1": 01, 362 | "tx_1": 11, 363 | "rx_2": 02, 364 | "tx_2": 12, 365 | "rx_3": 03, 366 | "tx_3": 13, 367 | "rx_4": 04, 368 | "tx_4": 14, 369 | }, 370 | } 371 | result, _ := json.Marshal(myRRD) 372 | fmt.Fprintln(w, string(result)) 373 | case "/error": 374 | myRRD := rrd{ 375 | Success: true, 376 | ErrorCode: "apps_denied", 377 | } 378 | result, _ := json.Marshal(myRRD) 379 | fmt.Fprintln(w, string(result)) 380 | case "/null": 381 | myRRD := rrd{ 382 | Success: true, 383 | } 384 | result, _ := json.Marshal(myRRD) 385 | fmt.Fprintln(w, string(result)) 386 | } 387 | })) 388 | defer ts.Close() 389 | 390 | goodPR := &postRequest{ 391 | method: "POST", 392 | header: "X-Fbx-App-Auth", 393 | url: ts.URL + "/good", 394 | } 395 | 396 | errorPR := &postRequest{ 397 | method: "POST", 398 | header: "X-Fbx-App-Auth", 399 | url: ts.URL + "/error", 400 | } 401 | 402 | nullPR := &postRequest{ 403 | method: "POST", 404 | header: "X-Fbx-App-Auth", 405 | url: ts.URL + "/null", 406 | } 407 | 408 | ai := &authInfo{} 409 | mySessionToken := "foobar" 410 | 411 | getSwitchResult, err := getSwitch(ai, goodPR, &mySessionToken) 412 | if err != nil { 413 | t.Error("Expected no err, but got", err) 414 | } 415 | 416 | if getSwitchResult[0] != 01 || getSwitchResult[1] != 11 || getSwitchResult[2] != 02 || getSwitchResult[3] != 12 || getSwitchResult[4] != 03 || getSwitchResult[5] != 13 || getSwitchResult[6] != 04 || getSwitchResult[7] != 14 { 417 | t.Errorf("Expected 01 11 02 12 03 13 04 14, but got %v %v %v %v %v %v %v %v\n", getSwitchResult[0], getSwitchResult[1], getSwitchResult[2], getSwitchResult[3], getSwitchResult[4], getSwitchResult[5], getSwitchResult[6], getSwitchResult[7]) 418 | } 419 | 420 | getSwitchResult, err = getSwitch(ai, errorPR, &mySessionToken) 421 | if err.Error() != "API access from apps has been disabled" { 422 | t.Error("Expected API access from apps has been disabled, but got", err) 423 | } 424 | 425 | if len(getSwitchResult) != 0 { 426 | t.Error("Expected 0, but got", len(getSwitchResult)) 427 | } 428 | 429 | getSwitchResult, err = getSwitch(ai, nullPR, &mySessionToken) 430 | if err != nil { 431 | t.Error("Expected no err, but got", err) 432 | } 433 | 434 | if len(getSwitchResult) != 0 { 435 | t.Error("Expected 0, but got", len(getSwitchResult)) 436 | } 437 | 438 | } 439 | 440 | func TestGetLan(t *testing.T) { 441 | os.Setenv("FREEBOX_TOKEN", "IOI") 442 | defer os.Unsetenv("FREEBOX_TOKEN") 443 | 444 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 445 | switch r.RequestURI { 446 | case "/good": 447 | myLan := lan{ 448 | Success: true, 449 | } 450 | myLan.Result = []lanHost{ 451 | { 452 | Reachable: true, 453 | PrimaryName: "Reachable host", 454 | }, 455 | { 456 | Reachable: false, 457 | PrimaryName: "Unreachable host", 458 | }, 459 | } 460 | result, _ := json.Marshal(myLan) 461 | fmt.Fprintln(w, string(result)) 462 | case "/error": 463 | myLan := lan{ 464 | Success: true, 465 | ErrorCode: "ratelimited", 466 | } 467 | result, _ := json.Marshal(myLan) 468 | fmt.Fprintln(w, string(result)) 469 | } 470 | })) 471 | defer ts.Close() 472 | 473 | goodPR := &postRequest{ 474 | method: "GET", 475 | header: "X-Fbx-App-Auth", 476 | url: ts.URL + "/good", 477 | } 478 | 479 | errorPR := &postRequest{ 480 | method: "GET", 481 | header: "X-Fbx-App-Auth", 482 | url: ts.URL + "/error", 483 | } 484 | 485 | ai := &authInfo{} 486 | mySessionToken := "foobar" 487 | 488 | lanAvailable, err := getLan(ai, goodPR, &mySessionToken) 489 | if err != nil { 490 | t.Error("Expected no err, but got", err) 491 | } 492 | 493 | for _, v := range lanAvailable { 494 | if v.Reachable && v.PrimaryName != "Reachable host" { 495 | t.Errorf("Expected Reachable: true, Host: Reachable host, but go Reachable: %v, Host: %v", v.Reachable, v.PrimaryName) 496 | } 497 | 498 | if !v.Reachable && v.PrimaryName != "Unreachable host" { 499 | t.Errorf("Expected Reachable: false, Host: Unreachable host, but go Reachable: %v, Host: %v", !v.Reachable, v.PrimaryName) 500 | } 501 | } 502 | 503 | lanAvailable, err = getLan(ai, errorPR, &mySessionToken) 504 | if err.Error() != "Too many auth error have been made from your IP" { 505 | t.Error("Expected Too many auth error have been made from your IP, but got", err) 506 | } 507 | 508 | } 509 | 510 | func TestGetSystem(t *testing.T) { 511 | os.Setenv("FREEBOX_TOKEN", "IOI") 512 | defer os.Unsetenv("FREEBOX_TOKEN") 513 | 514 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 515 | mySys := system{ 516 | Success: true, 517 | } 518 | mySys.Result.FanRPM = 666 519 | mySys.Result.TempCpub = 81 520 | mySys.Result.TempCpum = 89 521 | mySys.Result.TempHDD = 30 522 | mySys.Result.TempSW = 54 523 | 524 | /* 525 | mySys.Result { 526 | FanRPM: 666, 527 | TempCpub: 81, 528 | TempCpum: 89, 529 | TempHDD: 30, 530 | TempSW: 54, 531 | } 532 | */ 533 | result, _ := json.Marshal(mySys) 534 | fmt.Fprintln(w, string(result)) 535 | })) 536 | defer ts.Close() 537 | 538 | pr := &postRequest{ 539 | method: "GET", 540 | header: "X-Fbx-App-Auth", 541 | url: ts.URL, 542 | } 543 | 544 | ai := &authInfo{} 545 | mySessionToken := "foobar" 546 | 547 | systemStats, err := getSystem(ai, pr, &mySessionToken) 548 | if err != nil { 549 | t.Error("Expected no err, but got", err) 550 | } 551 | 552 | if systemStats.Result.FanRPM != 666 { 553 | t.Error("Expected 666, but got", systemStats.Result.FanRPM) 554 | } 555 | 556 | if systemStats.Result.TempCpub != 81 { 557 | t.Error("Expected 81, but got", systemStats.Result.TempCpub) 558 | } 559 | 560 | if systemStats.Result.TempCpum != 89 { 561 | t.Error("Expected 89, but got", systemStats.Result.TempCpum) 562 | } 563 | 564 | if systemStats.Result.TempHDD != 30 { 565 | t.Error("Expected 30, but got", systemStats.Result.TempHDD) 566 | } 567 | 568 | if systemStats.Result.TempSW != 54 { 569 | t.Error("Expected 54, but got", systemStats.Result.TempSW) 570 | } 571 | 572 | } 573 | 574 | func TestGetWifi(t *testing.T) { 575 | os.Setenv("FREEBOX_TOKEN", "IOI") 576 | defer os.Unsetenv("FREEBOX_TOKEN") 577 | 578 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 579 | myWifi := wifi{ 580 | Success: true, 581 | } 582 | myAP := wifiAccessPoint{ 583 | Name: "AP1", 584 | ID: 0, 585 | } 586 | myWifi.Result = []wifiAccessPoint{myAP} 587 | 588 | result, _ := json.Marshal(myWifi) 589 | fmt.Fprintln(w, string(result)) 590 | })) 591 | defer ts.Close() 592 | 593 | pr := &postRequest{ 594 | method: "GET", 595 | header: "X-Fbx-App-Auth", 596 | url: ts.URL, 597 | } 598 | 599 | ai := &authInfo{} 600 | mySessionToken := "foobar" 601 | 602 | wifiStats, err := getWifi(ai, pr, &mySessionToken) 603 | if err != nil { 604 | t.Error("Expected no err, but got", err) 605 | } 606 | 607 | if wifiStats.Result[0].Name != "AP1" { 608 | t.Error("Expected AP1, but got", wifiStats.Result[0].Name) 609 | } 610 | 611 | if wifiStats.Result[0].ID != 0 { 612 | t.Error("Expected 0, but got", wifiStats.Result[0].ID) 613 | } 614 | 615 | } 616 | 617 | func TestGetWifiStations(t *testing.T) { 618 | os.Setenv("FREEBOX_TOKEN", "IOI") 619 | defer os.Unsetenv("FREEBOX_TOKEN") 620 | 621 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 622 | myWifiStations := wifiStations{ 623 | Success: true, 624 | } 625 | 626 | myStation := wifiStation{ 627 | Hostname: "station_host", 628 | MAC: "AA:BB:CC:DD:EE:FF", 629 | State: "authorized", 630 | Inactive: 60, 631 | RXBytes: 500, 632 | TXBytes: 2280000000, 633 | ConnectionDuration: 600, 634 | TXRate: 4260000000, 635 | RXRate: 5, 636 | Signal: -20, 637 | } 638 | myWifiStations.Result = []wifiStation{myStation} 639 | 640 | result, _ := json.Marshal(myWifiStations) 641 | fmt.Fprintln(w, string(result)) 642 | })) 643 | defer ts.Close() 644 | 645 | pr := &postRequest{ 646 | method: "GET", 647 | header: "X-Fbx-App-Auth", 648 | url: ts.URL, 649 | } 650 | 651 | ai := &authInfo{} 652 | mySessionToken := "foobar" 653 | 654 | wifiStationsStats, err := getWifiStations(ai, pr, &mySessionToken) 655 | if err != nil { 656 | t.Error("Expected no err, but got", err) 657 | } 658 | 659 | if wifiStationsStats.Result[0].Hostname != "station_host" { 660 | t.Error("Expected station_host, but got", wifiStationsStats.Result[0].Hostname) 661 | } 662 | 663 | if wifiStationsStats.Result[0].MAC != "AA:BB:CC:DD:EE:FF" { 664 | t.Error("Expected AA:BB:CC:DD:EE:FF, but got", wifiStationsStats.Result[0].MAC) 665 | } 666 | 667 | if wifiStationsStats.Result[0].State != "authorized" { 668 | t.Error("Expected authorized, but got", wifiStationsStats.Result[0].State) 669 | } 670 | 671 | if wifiStationsStats.Result[0].Inactive != 60 { 672 | t.Error("Expected 60, but got", wifiStationsStats.Result[0].Inactive) 673 | } 674 | 675 | if wifiStationsStats.Result[0].RXBytes != 500 { 676 | t.Error("Expected 500, but got", wifiStationsStats.Result[0].RXBytes) 677 | } 678 | 679 | if wifiStationsStats.Result[0].TXBytes != 10000 { 680 | t.Error("Expected 10000, but got", wifiStationsStats.Result[0].TXBytes) 681 | } 682 | 683 | if wifiStationsStats.Result[0].ConnectionDuration != 600 { 684 | t.Error("Expected 600, but got", wifiStationsStats.Result[0].ConnectionDuration) 685 | } 686 | 687 | if wifiStationsStats.Result[0].TXRate != 20 { 688 | t.Error("Expected 20, but got", wifiStationsStats.Result[0].TXRate) 689 | } 690 | 691 | if wifiStationsStats.Result[0].RXRate != 5 { 692 | t.Error("Expected 5, but got", wifiStationsStats.Result[0].RXRate) 693 | } 694 | 695 | if wifiStationsStats.Result[0].Signal != -20 { 696 | t.Error("Expected -20, but got", wifiStationsStats.Result[0].Signal) 697 | } 698 | 699 | } 700 | 701 | func Test_getNet(t *testing.T) { 702 | type args struct { 703 | authInf *authInfo 704 | pr *postRequest 705 | xSessionToken *string 706 | } 707 | tests := []struct { 708 | name string 709 | args args 710 | want []int 711 | wantErr bool 712 | }{ 713 | // TODO: Add test cases. 714 | } 715 | for _, tt := range tests { 716 | t.Run(tt.name, func(t *testing.T) { 717 | got, err := getNet(tt.args.authInf, tt.args.pr, tt.args.xSessionToken) 718 | if (err != nil) != tt.wantErr { 719 | t.Errorf("getNet() error = %v, wantErr %v", err, tt.wantErr) 720 | return 721 | } 722 | if !reflect.DeepEqual(got, tt.want) { 723 | t.Errorf("getNet() = %v, want %v", got, tt.want) 724 | } 725 | }) 726 | } 727 | } 728 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module freebox_exporter 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c // indirect 7 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 8 | github.com/prometheus/client_golang v0.9.2 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 2 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 3 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 4 | github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c h1:fQ4P1oAipLwec/j5tfZTYV/e5i9ICSk23uVL+TK9III= 5 | github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= 6 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= 7 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= 8 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 9 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 10 | github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= 11 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 12 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= 13 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 14 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= 15 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 16 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= 17 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 18 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 19 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 20 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= 22 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "log" 7 | "net/http" 8 | "os" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/iancoleman/strcase" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | ) 18 | 19 | var ( 20 | mafreebox string 21 | listen string 22 | debug bool 23 | fiber bool 24 | ) 25 | 26 | func init() { 27 | flag.StringVar(&mafreebox, "endpoint", "http://mafreebox.freebox.fr/", "Endpoint for freebox API") 28 | flag.StringVar(&listen, "listen", ":10001", "Prometheus metrics port") 29 | flag.BoolVar(&debug, "debug", false, "Debug mode") 30 | flag.BoolVar(&fiber, "fiber", false, "Turn on if you're using a fiber Freebox") 31 | } 32 | 33 | func main() { 34 | flag.Parse() 35 | 36 | if !strings.HasSuffix(mafreebox, "/") { 37 | mafreebox = mafreebox + "/" 38 | } 39 | 40 | endpoint := mafreebox + "api/v4/login/" 41 | myAuthInfo := &authInfo{ 42 | myAPI: api{ 43 | login: endpoint, 44 | authz: endpoint + "authorize/", 45 | loginSession: endpoint + "session/", 46 | }, 47 | myStore: store{location: os.Getenv("HOME") + "/.freebox_token"}, 48 | myApp: app{ 49 | AppID: "fr.freebox.exporter", 50 | AppName: "prometheus-exporter", 51 | AppVersion: "0.4", 52 | DeviceName: "local", 53 | }, 54 | myReader: bufio.NewReader(os.Stdin), 55 | } 56 | 57 | myPostRequest := newPostRequest() 58 | 59 | myConnectionXdslRequest := &postRequest{ 60 | method: "GET", 61 | url: mafreebox + "api/v4/connection/xdsl/", 62 | header: "X-Fbx-App-Auth", 63 | } 64 | 65 | myFreeplugRequest := &postRequest{ 66 | method: "GET", 67 | url: mafreebox + "api/v4/freeplug/", 68 | header: "X-Fbx-App-Auth", 69 | } 70 | 71 | myLanRequest := &postRequest{ 72 | method: "GET", 73 | url: mafreebox + "api/v4/lan/browser/pub/", 74 | header: "X-Fbx-App-Auth", 75 | } 76 | 77 | mySystemRequest := &postRequest{ 78 | method: "GET", 79 | url: mafreebox + "api/v4/system/", 80 | header: "X-Fbx-App-Auth", 81 | } 82 | 83 | myWifiRequest := &postRequest{ 84 | method: "GET", 85 | url: mafreebox + "api/v2/wifi/ap/", 86 | header: "X-Fbx-App-Auth", 87 | } 88 | 89 | myVpnRequest := &postRequest{ 90 | method: "GET", 91 | url: mafreebox + "api/v4/vpn/connection/", 92 | header: "X-Fbx-App-Auth", 93 | } 94 | 95 | var mySessionToken string 96 | 97 | go func() { 98 | for { 99 | // There is no DSL metric on fiber Freebox 100 | // If you use a fiber Freebox, use -fiber flag to turn off this metric 101 | if !fiber { 102 | // connectionXdsl metrics 103 | connectionXdslStats, err := getConnectionXdsl(myAuthInfo, myConnectionXdslRequest, &mySessionToken) 104 | if err != nil { 105 | log.Printf("An error occured with connectionXdsl metrics: %v", err) 106 | } 107 | 108 | if connectionXdslStats.Success { 109 | status := connectionXdslStats.Result.Status 110 | result := connectionXdslStats.Result 111 | down := result.Down 112 | up := result.Up 113 | 114 | connectionXdslStatusUptimeGauges. 115 | WithLabelValues(status.Status, status.Protocol, status.Modulation). 116 | Set(float64(status.Uptime)) 117 | 118 | connectionXdslDownAttnGauge.Set(float64(down.Attn10) / 10) 119 | connectionXdslUpAttnGauge.Set(float64(up.Attn10) / 10) 120 | 121 | // XXX: sometimes the Freebox is reporting zero as SNR which 122 | // does not make sense so we don't log these 123 | if down.Snr10 > 0 { 124 | connectionXdslDownSnrGauge.Set(float64(down.Snr10) / 10) 125 | } 126 | if up.Snr10 > 0 { 127 | connectionXdslUpSnrGauge.Set(float64(up.Snr10) / 10) 128 | } 129 | 130 | connectionXdslNitroGauges.WithLabelValues("down"). 131 | Set(bool2float(down.Nitro)) 132 | connectionXdslNitroGauges.WithLabelValues("up"). 133 | Set(bool2float(up.Nitro)) 134 | 135 | connectionXdslGinpGauges.WithLabelValues("down", "enabled"). 136 | Set(bool2float(down.Ginp)) 137 | connectionXdslGinpGauges.WithLabelValues("up", "enabled"). 138 | Set(bool2float(up.Ginp)) 139 | 140 | logFields(result, connectionXdslGinpGauges, 141 | []string{"rtx_tx", "rtx_c", "rtx_uc"}) 142 | 143 | logFields(result, connectionXdslErrorGauges, 144 | []string{"crc", "es", "fec", "hec", "ses"}) 145 | } 146 | 147 | // dsl metrics 148 | getDslResult, err := getDsl(myAuthInfo, myPostRequest, &mySessionToken) 149 | if err != nil { 150 | log.Printf("An error occured with DSL metrics: %v", err) 151 | } 152 | 153 | if len(getDslResult) > 0 { 154 | rateUpGauge.Set(float64(getDslResult[0])) 155 | rateDownGauge.Set(float64(getDslResult[1])) 156 | snrUpGauge.Set(float64(getDslResult[2])) 157 | snrDownGauge.Set(float64(getDslResult[3])) 158 | } 159 | } 160 | 161 | // freeplug metrics 162 | freeplugStats, err := getFreeplug(myAuthInfo, myFreeplugRequest, &mySessionToken) 163 | if err != nil { 164 | log.Printf("An error occured with freeplug metrics: %v", err) 165 | } 166 | 167 | for _, freeplugNetwork := range freeplugStats.Result { 168 | for _, freeplugMember := range freeplugNetwork.Members { 169 | if freeplugMember.HasNetwork { 170 | freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(1)) 171 | } else { 172 | freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(0)) 173 | } 174 | 175 | Mb := 1e6 176 | rxRate := float64(freeplugMember.RxRate) * Mb 177 | txRate := float64(freeplugMember.TxRate) * Mb 178 | 179 | if rxRate >= 0 { // -1 if not unavailable 180 | freeplugRxRateGauge.WithLabelValues(freeplugMember.ID).Set(rxRate) 181 | } 182 | 183 | if txRate >= 0 { // -1 if not unavailable 184 | freeplugTxRateGauge.WithLabelValues(freeplugMember.ID).Set(txRate) 185 | } 186 | } 187 | } 188 | 189 | // net metrics 190 | getNetResult, err := getNet(myAuthInfo, myPostRequest, &mySessionToken) 191 | if err != nil { 192 | log.Printf("An error occured with NET metrics: %v", err) 193 | } 194 | 195 | if len(getNetResult) > 0 { 196 | bwUpGauge.Set(float64(getNetResult[0])) 197 | bwDownGauge.Set(float64(getNetResult[1])) 198 | netRateUpGauge.Set(float64(getNetResult[2])) 199 | netRateDownGauge.Set(float64(getNetResult[3])) 200 | vpnRateUpGauge.Set(float64(getNetResult[4])) 201 | vpnRateDownGauge.Set(float64(getNetResult[5])) 202 | } 203 | 204 | // lan metrics 205 | lanAvailable, err := getLan(myAuthInfo, myLanRequest, &mySessionToken) 206 | if err != nil { 207 | log.Printf("An error occured with LAN metrics: %v", err) 208 | } 209 | for _, v := range lanAvailable { 210 | var Ip string 211 | if len(v.L3c) > 0 { 212 | Ip = v.L3c[0].Addr 213 | } else { 214 | Ip = "" 215 | } 216 | if v.Reachable { 217 | lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(1)) 218 | } else { 219 | lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(0)) 220 | } 221 | } 222 | 223 | // system metrics 224 | systemStats, err := getSystem(myAuthInfo, mySystemRequest, &mySessionToken) 225 | if err != nil { 226 | log.Printf("An error occured with System metrics: %v", err) 227 | } 228 | 229 | systemTempGauges.WithLabelValues("Température CPU B").Set(float64(systemStats.Result.TempCpub)) 230 | systemTempGauges.WithLabelValues("Température CPU M").Set(float64(systemStats.Result.TempCpum)) 231 | systemTempGauges.WithLabelValues("Température Switch").Set(float64(systemStats.Result.TempSW)) 232 | systemTempGauges.WithLabelValues("Disque dur").Set(float64(systemStats.Result.TempHDD)) 233 | systemFanGauges.WithLabelValues("Ventilateur 1").Set(float64(systemStats.Result.FanRPM)) 234 | 235 | systemUptimeGauges. 236 | WithLabelValues(systemStats.Result.FirmwareVersion). 237 | Set(float64(systemStats.Result.UptimeVal)) 238 | 239 | // wifi metrics 240 | wifiStats, err := getWifi(myAuthInfo, myWifiRequest, &mySessionToken) 241 | if err != nil { 242 | log.Printf("An error occured with Wifi metrics: %v", err) 243 | } 244 | for _, accessPoint := range wifiStats.Result { 245 | myWifiStationRequest := &postRequest{ 246 | method: "GET", 247 | url: mafreebox + "api/v2/wifi/ap/" + strconv.Itoa(accessPoint.ID) + "/stations", 248 | header: "X-Fbx-App-Auth", 249 | } 250 | wifiStationsStats, err := getWifiStations(myAuthInfo, myWifiStationRequest, &mySessionToken) 251 | if err != nil { 252 | log.Printf("An error occured with Wifi station metrics: %v", err) 253 | } 254 | for _, station := range wifiStationsStats.Result { 255 | wifiSignalGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Signal)) 256 | wifiInactiveGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Inactive)) 257 | wifiConnectionDurationGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.ConnectionDuration)) 258 | wifiRXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXBytes)) 259 | wifiTXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXBytes)) 260 | wifiRXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXRate)) 261 | wifiTXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXRate)) 262 | } 263 | } 264 | 265 | // VPN Server Connections List 266 | getVpnServerResult, err := getVpnServer(myAuthInfo, myVpnRequest, &mySessionToken) 267 | if err != nil { 268 | log.Printf("An error occured with VPN station metrics: %v", err) 269 | } 270 | for _, connection := range getVpnServerResult.Result { 271 | vpnServerConnectionsList.With(prometheus.Labels{"user": connection.User, "vpn": connection.Vpn, "src_ip": connection.SrcIP, "local_ip": connection.LocalIP, "name": "rx_bytes"}).Set(float64(connection.RxBytes)) 272 | vpnServerConnectionsList.With(prometheus.Labels{"user": connection.User, "vpn": connection.Vpn, "src_ip": connection.SrcIP, "local_ip": connection.LocalIP, "name": "tx_bytes"}).Set(float64(connection.TxBytes)) 273 | } 274 | 275 | time.Sleep(10 * time.Second) 276 | } 277 | }() 278 | 279 | log.Println("freebox_exporter started on port", listen) 280 | http.Handle("/metrics", promhttp.Handler()) 281 | log.Fatal(http.ListenAndServe(listen, nil)) 282 | } 283 | 284 | func logFields(result interface{}, gauge *prometheus.GaugeVec, fields []string) error { 285 | resultReflect := reflect.ValueOf(result) 286 | 287 | for _, direction := range []string{"down", "up"} { 288 | for _, field := range fields { 289 | value := reflect.Indirect(resultReflect). 290 | FieldByName(strcase.ToCamel(direction)). 291 | FieldByName(strcase.ToCamel(field)) 292 | 293 | if value.IsZero() { 294 | continue 295 | } 296 | 297 | gauge.WithLabelValues(direction, field). 298 | Set(float64(value.Int())) 299 | } 300 | } 301 | 302 | return nil 303 | } 304 | 305 | func bool2float(b bool) float64 { 306 | if b { 307 | return 1 308 | } 309 | return 0 310 | } 311 | -------------------------------------------------------------------------------- /structs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "bufio" 4 | 5 | type track struct { 6 | Success bool `json:"success"` 7 | Result struct { 8 | AppToken string `json:"app_token"` 9 | TrackID int `json:"track_id"` 10 | } `json:"result"` 11 | } 12 | 13 | type grant struct { 14 | Success bool `json:"success"` 15 | Result struct { 16 | Status string `json:"status"` 17 | Challenge string `json:"challenge"` 18 | } `json:"result"` 19 | } 20 | 21 | type challenge struct { 22 | Success bool `json:"success"` 23 | Result struct { 24 | LoggedIN bool `json:"logged_in,omitempty"` 25 | Challenge string `json:"challenge"` 26 | } `json:"result"` 27 | } 28 | 29 | type session struct { 30 | AppID string `json:"app_id"` 31 | Password string `json:"password"` 32 | } 33 | 34 | type sessionToken struct { 35 | Msg string `json:"msg,omitempty"` 36 | Success bool `json:"success"` 37 | UID string `json:"uid,omitempty"` 38 | ErrorCode string `json:"error_code,omitempty"` 39 | Result struct { 40 | SessionToken string `json:"session_token,omitempty"` 41 | Challenge string `json:"challenge"` 42 | Permissions struct { 43 | Settings bool `json:"settings,omitempty"` 44 | Contacts bool `json:"contacts,omitempty"` 45 | Calls bool `json:"calls,omitempty"` 46 | Explorer bool `json:"explorer,omitempty"` 47 | Downloader bool `json:"downloader,omitempty"` 48 | Parental bool `json:"parental,omitempty"` 49 | Pvr bool `json:"pvr,omitempty"` 50 | Home bool `json:"home,omitempty"` 51 | Camera bool `json:"camera,omitempty"` 52 | } `json:"permissions,omitempty"` 53 | } `json:"result"` 54 | } 55 | 56 | type rrd struct { 57 | UID string `json:"uid,omitempty"` 58 | Success bool `json:"success"` 59 | Msg string `json:"msg,omitempty"` 60 | Result struct { 61 | DateStart int `json:"date_start,omitempty"` 62 | DateEnd int `json:"date_end,omitempty"` 63 | Data []map[string]int64 `json:"data,omitempty"` 64 | } `json:"result"` 65 | ErrorCode string `json:"error_code"` 66 | } 67 | 68 | // https://dev.freebox.fr/sdk/os/connection/ 69 | type connectionXdsl struct { 70 | Success bool `json:"success"` 71 | Result struct { 72 | Status struct { 73 | Status string `json:"status"` 74 | Modulation string `json:"modulation"` 75 | Protocol string `json:"protocol"` 76 | Uptime int `json:"uptime"` 77 | } `json:"status"` 78 | Down struct { 79 | Attn int `json:"attn"` 80 | Attn10 int `json:"attn_10"` 81 | Crc int `json:"crc"` 82 | Es int `json:"es"` 83 | Fec int `json:"fec"` 84 | Ginp bool `json:"ginp"` 85 | Hec int `json:"hec"` 86 | Maxrate uint64 `json:"maxrate"` 87 | Nitro bool `json:"nitro"` 88 | Phyr bool `json:"phyr"` 89 | Rate int `json:"rate"` 90 | RtxC int `json:"rtx_c,omitempty"` 91 | RtxTx int `json:"rtx_tx,omitempty"` 92 | RtxUc int `json:"rtx_uc,omitempty"` 93 | Rxmt int `json:"rxmt"` 94 | RxmtCorr int `json:"rxmt_corr"` 95 | RxmtUncorr int `json:"rxmt_uncorr"` 96 | Ses int `json:"ses"` 97 | Snr int `json:"snr"` 98 | Snr10 int `json:"snr_10"` 99 | } `json:"down"` 100 | Up struct { 101 | Attn int `json:"attn"` 102 | Attn10 int `json:"attn_10"` 103 | Crc int `json:"crc"` 104 | Es int `json:"es"` 105 | Fec int `json:"fec"` 106 | Ginp bool `json:"ginp"` 107 | Hec int `json:"hec"` 108 | Maxrate uint64 `json:"maxrate"` 109 | Nitro bool `json:"nitro"` 110 | Phyr bool `json:"phyr"` 111 | Rate uint64 `json:"rate"` 112 | RtxC int `json:"rtx_c,omitempty"` 113 | RtxTx int `json:"rtx_tx,omitempty"` 114 | RtxUc int `json:"rtx_uc,omitempty"` 115 | Rxmt int `json:"rxmt"` 116 | RxmtCorr int `json:"rxmt_corr"` 117 | RxmtUncorr int `json:"rxmt_uncorr"` 118 | Ses int `json:"ses"` 119 | Snr int `json:"snr"` 120 | Snr10 int `json:"snr_10"` 121 | } `json:"up"` 122 | } 123 | } 124 | 125 | type database struct { 126 | DB string `json:"db"` 127 | DateStart int `json:"date_start,omitempty"` 128 | DateEnd int `json:"date_end,omitempty"` 129 | Precision int `json:"precision,omitempty"` 130 | Fields []string `json:"fields"` 131 | } 132 | 133 | // https://dev.freebox.fr/sdk/os/freeplug/ 134 | type freeplug struct { 135 | Success bool `json:"success"` 136 | Result []freeplugNetwork `json:"result"` 137 | } 138 | 139 | type freeplugNetwork struct { 140 | ID string `json:"id"` 141 | Members []freeplugMember `json:"members"` 142 | } 143 | 144 | type freeplugMember struct { 145 | ID string `json:"id"` 146 | Local bool `json:"local"` 147 | NetRole string `json:"net_role"` 148 | EthPortStatus string `json:"eth_port_status"` 149 | EthFullDuplex bool `json:"eth_full_duplex"` 150 | HasNetwork bool `json:"has_network"` 151 | EthSpeed int `json:"eth_speed"` 152 | Inative int `json:"inactive"` 153 | NetID string `json:"net_id"` 154 | RxRate int64 `json:"rx_rate"` 155 | TxRate int64 `json:"tx_rate"` 156 | Model string `json:"model"` 157 | } 158 | 159 | // https://dev.freebox.fr/sdk/os/lan/ 160 | type l3c struct { 161 | Addr string `json:"addr,omitempty"` 162 | } 163 | 164 | type lanHost struct { 165 | Reachable bool `json:"reachable,omitempty"` 166 | PrimaryName string `json:"primary_name,omitempty"` 167 | Vendor_name string `json:"vendor_name,omitempty"` 168 | L3c []l3c `json:"l3connectivities,omitempty"` 169 | } 170 | 171 | type lan struct { 172 | Success bool `json:"success"` 173 | Result []lanHost `json:"result"` 174 | ErrorCode string `json:"error_code"` 175 | } 176 | 177 | type idNameValue struct { 178 | ID string `json:"id,omitempty"` 179 | Name string `json:"name,omitempty"` 180 | Value int `json:"value,omitempty"` 181 | } 182 | 183 | // https://dev.freebox.fr/sdk/os/system/ 184 | type system struct { 185 | Success bool `json:"success"` 186 | Result struct { 187 | Mac string `json:"mac,omitempty"` 188 | FanRPM int `json:"fan_rpm,omitempty"` 189 | BoxFlavor string `json:"box_flavor,omitempty"` 190 | TempCpub int `json:"temp_cpub,omitempty"` 191 | TempCpum int `json:"temp_cpum,omitempty"` 192 | DiskStatus string `json:"disk_status,omitempty"` 193 | TempHDD int `json:"temp_hdd,omitempty"` 194 | BoardName string `json:"board_name,omitempty"` 195 | TempSW int `json:"temp_sw,omitempty"` 196 | Uptime string `json:"uptime,omitempty"` 197 | UptimeVal int `json:"uptime_val,omitempty"` 198 | UserMainStorage string `json:"user_main_storage,omitempty"` 199 | BoxAuthenticated bool `json:"box_authenticated,omitempty"` 200 | Serial string `json:"serial,omitempty"` 201 | FirmwareVersion string `json:"firmware_version,omitempty"` 202 | } 203 | } 204 | 205 | // https://dev.freebox.fr/sdk/os/wifi/ 206 | type wifiAccessPoint struct { 207 | Name string `json:"name,omitempty"` 208 | ID int `json:"id,omitempty"` 209 | } 210 | 211 | type wifi struct { 212 | Success bool `json:"success"` 213 | Result []wifiAccessPoint `json:"result,omitempty"` 214 | } 215 | 216 | type wifiStation struct { 217 | Hostname string `json:"hostname,omitempty"` 218 | MAC string `json:"mac,omitempty"` 219 | State string `json:"state,omitempty"` 220 | Inactive int `json:"inactive,omitempty"` 221 | RXBytes int64 `json:"rx_bytes,omitempty"` 222 | TXBytes int64 `json:"tx_bytes,omitempty"` 223 | ConnectionDuration int `json:"conn_duration,omitempty"` 224 | TXRate int64 `json:"tx_rate,omitempty"` 225 | RXRate int64 `json:"rx_rate,omitempty"` 226 | Signal int `json:"signal,omitempty"` 227 | } 228 | 229 | type wifiStations struct { 230 | Success bool `json:"success"` 231 | Result []wifiStation `json:"result,omitempty"` 232 | } 233 | 234 | type app struct { 235 | AppID string `json:"app_id"` 236 | AppName string `json:"app_name"` 237 | AppVersion string `json:"app_version"` 238 | DeviceName string `json:"device_name"` 239 | } 240 | 241 | type api struct { 242 | authz string 243 | login string 244 | loginSession string 245 | } 246 | 247 | type store struct { 248 | location string 249 | } 250 | 251 | type authInfo struct { 252 | myApp app 253 | myAPI api 254 | myStore store 255 | myReader *bufio.Reader 256 | } 257 | 258 | type postRequest struct { 259 | method, url, header string 260 | } 261 | 262 | // https://dev.freebox.fr/sdk/os/vpn/ 263 | type vpnServer struct { 264 | Success bool `json:"success"` 265 | Result []struct { 266 | RxBytes int64 `json:"rx_bytes,omitempty"` 267 | Authenticated bool `json:"authenticated,omitempty"` 268 | TxBytes int64 `json:"tx_bytes,omitempty"` 269 | User string `json:"user,omitempty"` 270 | ID string `json:"id,omitempty"` 271 | Vpn string `json:"vpn,omitempty"` 272 | SrcIP string `json:"src_ip,omitempty"` 273 | AuthTime int32 `json:"auth_time,omitempty"` 274 | LocalIP string `json:"local_ip,omitempty"` 275 | } `json:"result,omitempty"` 276 | } 277 | --------------------------------------------------------------------------------