From fcc14326c2246416c1091a6d09239e0c5d32f4ff Mon Sep 17 00:00:00 2001 From: Uzurka Date: Mon, 26 Jun 2023 19:37:38 +0200 Subject: [PATCH] first commit --- .github/workflows/amd64.yml | 36 + .github/workflows/armv7.yml | 36 + .github/workflows/codeql-analysis.yml | 71 ++ .gitignore | 1 + Dockerfile-armv7 | 19 + Dockerfile.amd64 | 28 + LICENSE | 201 +++++ README.md | 91 +++ authz.go | 227 ++++++ authz_test.go | 413 ++++++++++ build.sh | 8 + changelog.md | 59 ++ contrib/README.md | 9 + .../lepichon_freebox_grafana_dashboard.json | 544 +++++++++++++ .../mcanevet_freebox_grafana_dashboard.json | 418 ++++++++++ gauges.go | 255 ++++++ getters.go | 573 ++++++++++++++ getters_test.go | 727 ++++++++++++++++++ go.mod | 9 + go.sum | 23 + main.go | 310 ++++++++ structs.go | 276 +++++++ 22 files changed, 4334 insertions(+) create mode 100644 .github/workflows/amd64.yml create mode 100644 .github/workflows/armv7.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .gitignore create mode 100644 Dockerfile-armv7 create mode 100644 Dockerfile.amd64 create mode 100644 LICENSE create mode 100644 README.md create mode 100644 authz.go create mode 100644 authz_test.go create mode 100755 build.sh create mode 100644 changelog.md create mode 100644 contrib/README.md create mode 100644 contrib/lepichon_freebox_grafana_dashboard.json create mode 100644 contrib/mcanevet_freebox_grafana_dashboard.json create mode 100644 gauges.go create mode 100644 getters.go create mode 100644 getters_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 structs.go diff --git a/.github/workflows/amd64.yml b/.github/workflows/amd64.yml new file mode 100644 index 0000000..2c50eb3 --- /dev/null +++ b/.github/workflows/amd64.yml @@ -0,0 +1,36 @@ +name: amd64 + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check Out Repo + uses: actions/checkout@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile-amd64 + push: true + tags: saphoooo/freebox-exporter + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/armv7.yml b/.github/workflows/armv7.yml new file mode 100644 index 0000000..c31f0fe --- /dev/null +++ b/.github/workflows/armv7.yml @@ -0,0 +1,36 @@ +name: armv7 + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check Out Repo + uses: actions/checkout@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile-armv7 + push: true + tags: saphoooo/freebox-exporter:armv7 + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..3b3e9db --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '44 5 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # 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 + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b86cae3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/freebox_exporter diff --git a/Dockerfile-armv7 b/Dockerfile-armv7 new file mode 100644 index 0000000..71877e9 --- /dev/null +++ b/Dockerfile-armv7 @@ -0,0 +1,19 @@ +FROM golang:1.14 + +WORKDIR / + +COPY . . + +RUN set -x && \ + go get -d -v . && \ + CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags "-w -s" -a -installsuffix cgo -o app . + +FROM scratch + +LABEL maintainer="stephane.beuret@gmail.com" + +COPY --from=0 app / + +COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +ENTRYPOINT ["/app"] diff --git a/Dockerfile.amd64 b/Dockerfile.amd64 new file mode 100644 index 0000000..e5ad52a --- /dev/null +++ b/Dockerfile.amd64 @@ -0,0 +1,28 @@ +FROM golang:1.14 + +WORKDIR / + +COPY . . + +ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz /usr/local + +RUN set -x && \ + apt update && \ + apt install -y xz-utils && \ + xz -d -c /usr/local/upx-3.95-amd64_linux.tar.xz | \ + tar -xOf - upx-3.95-amd64_linux/upx > /bin/upx && \ + chmod a+x /bin/upx && \ + go get -d -v . && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o app . && \ + strip --strip-unneeded app && \ + upx app + +FROM scratch + +LABEL maintainer="stephane.beuret@gmail.com" + +COPY --from=0 app / + +COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +ENTRYPOINT ["/app"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3744469 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# freebox_exporter + +A Prometheus exporter for Freebox stats + +## Cmds + +`freebox_exporter` + +## Flags + +- `-endpoint`: Freebox API url (default http://mafreebox.freebox.fr) +- `-listen`: port for Prometheus metrics (default :10001) +- `-debug`: turn on debug mode +- `-fiber`: turn off DSL metric for fiber Freebox + +## Preview + +Here's what you can get in Prometheus / Grafana with freebox_exporter: + +![Preview](https://user-images.githubusercontent.com/13923756/54585380-33318800-4a1a-11e9-8e9d-e434f275755c.png) + +# How to use it + +## Compiled binary + +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`. + +You can also find the compiled binaries for MacOS, Linux (x86_64, arm64 and arm) and Windows in the release tab. + +### Quick start + +``` +./freebox_exporter +``` + +### The following parameters are optional and can be overridden: + +- Freebox API endpoint + +``` +./freebox_exporter -endpoint "http://mafreebox.freebox.fr" +``` + +- Port + +``` +./freebox_exporter -listen ":10001" +``` + +## Docker + +### Quick start + +``` +docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \ + saphoooo/freebox-exporter +``` + +### The following parameters are optional and can be overridden: + +- Local token + +Volume allows to save the access token outside of the container to reuse authentication upon an update of the container. + +``` +docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \ + -e HOME=token -v /path/to/token:/token saphoooo/freebox-exporter +``` + +- Freebox API endpoint + +``` +docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 + saphoooo/freebox-exporter -endpoint "http://mafreebox.freebox.fr" +``` + +- Port + +``` +docker run -d --name freebox-exporter --restart on-failure -p 8080:10001 \ + saphoooo/freebox-exporter +``` + +## Caution on first run + +If you launch the application for the first time, you must allow it to access the freebox API. +- The application must be launched from the local network. +- You have to authorize the application from the freebox front panel. +- You have to modify the rights of the application to give it "Modification des réglages de la Freebox" + +Source: https://dev.freebox.fr/sdk/os/ diff --git a/authz.go b/authz.go new file mode 100644 index 0000000..1ab03f1 --- /dev/null +++ b/authz.go @@ -0,0 +1,227 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "os" + "strconv" + "time" +) + +// storeToken stores app_token in ~/.freebox_token +func storeToken(token string, authInf *authInfo) error { + err := os.Setenv("FREEBOX_TOKEN", token) + if err != nil { + return err + } + + if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) { + err := ioutil.WriteFile(authInf.myStore.location, []byte(token), 0600) + if err != nil { + return err + } + } + + return nil +} + +// retreiveToken gets the token from file and +// load it in environment variable +func retreiveToken(authInf *authInfo) (string, error) { + if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) { + return "", err + } + data, err := ioutil.ReadFile(authInf.myStore.location) + if err != nil { + return "", err + } + err = os.Setenv("FREEBOX_TOKEN", string(data)) + if err != nil { + return "", err + } + return string(data), nil +} + +// getTrackID is the initial request to freebox API +// get app_token and track_id +func getTrackID(authInf *authInfo) (*track, error) { + req, _ := json.Marshal(authInf.myApp) + buf := bytes.NewReader(req) + resp, err := http.Post(authInf.myAPI.authz, "application/json", buf) + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + trackID := track{} + err = json.Unmarshal(body, &trackID) + if err != nil { + return nil, err + } + + err = storeToken(trackID.Result.AppToken, authInf) + if err != nil { + return nil, err + } + + return &trackID, nil +} + +// getGranted waits for user to validate on the freebox front panel +// with a timeout of 15 seconds +func getGranted(authInf *authInfo) error { + trackID, err := getTrackID(authInf) + if err != nil { + return err + } + + url := authInf.myAPI.authz + strconv.Itoa(trackID.Result.TrackID) + for i := 0; i < 15; i++ { + resp, err := http.Get(url) + if err != nil { + return err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + granted := grant{} + err = json.Unmarshal(body, &granted) + if err != nil { + return err + } + + switch granted.Result.Status { + case "unknown": + return errors.New("the app_token is invalid or has been revoked") + case "pending": + log.Println("the user has not confirmed the authorization request yet") + case "timeout": + return errors.New("the user did not confirmed the authorization within the given time") + case "granted": + log.Println("the app_token is valid and can be used to open a session") + i = 15 + case "denied": + return errors.New("the user denied the authorization request") + } + time.Sleep(1 * time.Second) + } + return nil +} + +// getChallenge makes sure the app always has a valid challenge +func getChallenge(authInf *authInfo) (*challenge, error) { + resp, err := http.Get(authInf.myAPI.login) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + challenged := challenge{} + err = json.Unmarshal(body, &challenged) + if err != nil { + return nil, err + } + return &challenged, nil +} + +// hmacSha1 encodes app_token in hmac-sha1 and stores it in password +func hmacSha1(appToken, challenge string) string { + hash := hmac.New(sha1.New, []byte(appToken)) + hash.Write([]byte(challenge)) + return hex.EncodeToString(hash.Sum(nil)) +} + +// getSession gets a session with freeebox API +func getSession(authInf *authInfo, passwd string) (*sessionToken, error) { + s := session{ + AppID: authInf.myApp.AppID, + Password: passwd, + } + req, err := json.Marshal(s) + if err != nil { + return nil, err + } + buf := bytes.NewReader(req) + resp, err := http.Post(authInf.myAPI.loginSession, "application/json", buf) + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + token := sessionToken{} + err = json.Unmarshal(body, &token) + if err != nil { + return nil, err + } + return &token, nil +} + +// getToken gets a valid session_token and asks for user to change +// the set of permissions on the API +func getToken(authInf *authInfo, xSessionToken *string) (string, error) { + if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) { + err = getGranted(authInf) + if err != nil { + return "", err + } + + reader := authInf.myReader + log.Println("check \"Modification des réglages de la Freebox\" and press enter") + _, err = reader.ReadString('\n') + if err != nil { + return "", err + } + } else { + _, err := retreiveToken(authInf) + if err != nil { + return "", err + } + } + + token, err := getSessToken(os.Getenv("FREEBOX_TOKEN"), authInf, xSessionToken) + if err != nil { + return "", err + } + *xSessionToken = token + return token, nil +} + +// getSessToken gets a new token session when the old one has expired +func getSessToken(token string, authInf *authInfo, xSessionToken *string) (string, error) { + challenge, err := getChallenge(authInf) + if err != nil { + return "", err + } + password := hmacSha1(token, challenge.Result.Challenge) + t, err := getSession(authInf, password) + if err != nil { + return "", err + } + if t.Success == false { + return "", errors.New(t.Msg) + } + *xSessionToken = t.Result.SessionToken + return t.Result.SessionToken, nil +} diff --git a/authz_test.go b/authz_test.go new file mode 100644 index 0000000..b70cc83 --- /dev/null +++ b/authz_test.go @@ -0,0 +1,413 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" +) + +func TestRetreiveToken(t *testing.T) { + ai := &authInfo{ + myStore: store{location: "/tmp/token"}, + } + + _, err := retreiveToken(ai) + if err.Error() != "stat /tmp/token: no such file or directory" { + t.Error("Expected bla, but got", err) + } + + ioutil.WriteFile(ai.myStore.location, []byte("IOI"), 0600) + defer os.Remove(ai.myStore.location) + + token, err := retreiveToken(ai) + if err != nil { + t.Error("Expected no err, but got", err) + } + + newToken := os.Getenv("FREEBOX_TOKEN") + + if newToken != "IOI" { + t.Error("Expected IOI, but got", newToken) + } + + if token != "IOI" { + t.Error("Expected IOI, but got", newToken) + } + + os.Unsetenv("FREEBOX_TOKEN") +} + +func TestStoreToken(t *testing.T) { + var token string + + ai := &authInfo{} + token = "IOI" + err := storeToken(token, ai) + if err.Error() != "open : no such file or directory" { + t.Error("Expected open : no such file or directory, but got", err) + } + + ai.myStore.location = "/tmp/token" + err = storeToken(token, ai) + if err != nil { + t.Error("Expected no err, but got", err) + } + defer os.Remove(ai.myStore.location) + + token = os.Getenv("FREEBOX_TOKEN") + if token != "IOI" { + t.Error("Expected IOI, but got", token) + } + os.Unsetenv("FREEBOX_TOKEN") + + data, err := ioutil.ReadFile(ai.myStore.location) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if string(data) != "IOI" { + t.Error("Expected IOI, but got", string(data)) + } + +} + +func TestGetTrackID(t *testing.T) { + ai := &authInfo{ + myStore: store{location: "/tmp/token"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + myTrack := track{ + Success: true, + } + myTrack.Result.AppToken = "IOI" + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + })) + defer ts.Close() + + ai.myAPI.authz = ts.URL + trackID, err := getTrackID(ai) + if err != nil { + t.Error("Expected no err, but got", err) + } + defer os.Remove(ai.myStore.location) + defer os.Unsetenv("FREEBOX_TOKEN") + + if trackID.Result.TrackID != 101 { + t.Error("Expected 101, but got", trackID.Result.TrackID) + } + + // as getTrackID have no return value + // the result of storeToken func is checked instead + token := os.Getenv("FREEBOX_TOKEN") + if token != "IOI" { + t.Error("Expected IOI, but got", token) + } +} + +func TestGetGranted(t *testing.T) { + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/unknown/": + myTrack := track{ + Success: true, + } + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + case "/unknown/101": + myGrant := grant{ + Success: true, + } + myGrant.Result.Status = "unknown" + result, _ := json.Marshal(myGrant) + fmt.Fprintln(w, string(result)) + case "/timeout/": + myTrack := track{ + Success: true, + } + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + case "/timeout/101": + myGrant := grant{ + Success: true, + } + myGrant.Result.Status = "timeout" + result, _ := json.Marshal(myGrant) + fmt.Fprintln(w, string(result)) + case "/denied/": + myTrack := track{ + Success: true, + } + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + case "/denied/101": + myGrant := grant{ + Success: true, + } + myGrant.Result.Status = "denied" + result, _ := json.Marshal(myGrant) + fmt.Fprintln(w, string(result)) + case "/granted/": + myTrack := track{ + Success: true, + } + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + case "/granted/101": + myGrant := grant{ + Success: true, + } + myGrant.Result.Status = "granted" + result, _ := json.Marshal(myGrant) + fmt.Fprintln(w, string(result)) + default: + fmt.Fprintln(w, http.StatusNotFound) + } + })) + defer ts.Close() + + ai := authInfo{} + ai.myAPI.authz = ts.URL + "/unknown/" + ai.myStore.location = "/tmp/token" + + err := getGranted(&ai) + if err.Error() != "the app_token is invalid or has been revoked" { + t.Error("Expected the app_token is invalid or has been revoked, but got", err) + } + defer os.Remove(ai.myStore.location) + defer os.Unsetenv("FREEBOX_TOKEN") + + ai.myAPI.authz = ts.URL + "/timeout/" + err = getGranted(&ai) + if err.Error() != "the user did not confirmed the authorization within the given time" { + t.Error("Expected the user did not confirmed the authorization within the given time, but got", err) + } + + ai.myAPI.authz = ts.URL + "/denied/" + err = getGranted(&ai) + if err.Error() != "the user denied the authorization request" { + t.Error("Expected the user denied the authorization request, but got", err) + } + + ai.myAPI.authz = ts.URL + "/granted/" + err = getGranted(&ai) + if err != nil { + t.Error("Expected no err, but got", err) + } +} + +func TestGetChallenge(t *testing.T) { + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + myChall := &challenge{ + Success: true, + } + myChall.Result.Challenge = "foobar" + result, _ := json.Marshal(myChall) + fmt.Fprintln(w, string(result)) + })) + defer ts.Close() + + ai := &authInfo{ + myAPI: api{ + login: ts.URL, + }, + } + + challenged, err := getChallenge(ai) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if challenged.Success != true { + t.Error("Expected true, but got", challenged.Success) + } + + if challenged.Result.Challenge != "foobar" { + t.Error("Expected foobar, but got", challenged.Result.Challenge) + } +} + +func TestHmacSha1(t *testing.T) { + hmac := hmacSha1("IOI", "foobar") + if hmac != "02fb876a39b64eddcfee3eaa69465cb3e8d53cde" { + t.Error("Expected 02fb876a39b64eddcfee3eaa69465cb3e8d53cde, but got", hmac) + } +} + +func TestGetSession(t *testing.T) { + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + myToken := &sessionToken{ + Success: true, + } + myToken.Result.Challenge = "foobar" + result, _ := json.Marshal(myToken) + fmt.Fprintln(w, string(result)) + })) + defer ts.Close() + + ai := &authInfo{ + myAPI: api{ + loginSession: ts.URL, + }, + } + + token, err := getSession(ai, "") + if err != nil { + t.Error("Expected no err, but got", err) + } + defer os.Unsetenv("FREEBOX_TOKEN") + + if token.Success != true { + t.Error("Expected true, but got", token.Success) + } + + if token.Result.Challenge != "foobar" { + t.Error("Expected foobar, but got", token.Result.Challenge) + } +} + +func TestGetToken(t *testing.T) { + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/login": + myChall := &challenge{ + Success: true, + } + myChall.Result.Challenge = "foobar" + result, _ := json.Marshal(myChall) + fmt.Fprintln(w, string(result)) + case "/session": + myToken := sessionToken{ + Success: true, + } + myToken.Result.SessionToken = "foobar" + result, _ := json.Marshal(myToken) + fmt.Fprintln(w, string(result)) + case "/granted/": + myTrack := track{ + Success: true, + } + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + case "/granted/101": + myGrant := grant{ + Success: true, + } + myGrant.Result.Status = "granted" + result, _ := json.Marshal(myGrant) + fmt.Fprintln(w, string(result)) + default: + fmt.Fprintln(w, http.StatusNotFound) + } + })) + defer ts.Close() + + ai := authInfo{} + ai.myStore.location = "/tmp/token" + ai.myAPI.login = ts.URL + "/login" + ai.myAPI.loginSession = ts.URL + "/session" + ai.myAPI.authz = ts.URL + "/granted/" + ai.myReader = bufio.NewReader(strings.NewReader("\n")) + + var mySessionToken string + + // the first pass valide getToken without a token stored in a file + tk, err := getToken(&ai, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + defer os.Remove(ai.myStore.location) + defer os.Unsetenv("FREEBOX_TOKEN") + + if mySessionToken != "foobar" { + t.Error("Expected foobar, but got", mySessionToken) + } + + if tk != "foobar" { + t.Error("Expected foobar, but got", tk) + } + + // the second pass validate getToken with a token stored in a file: + // the first pass creates a file at ai.myStore.location + tk, err = getToken(&ai, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if mySessionToken != "foobar" { + t.Error("Expected foobar, but got", mySessionToken) + } + + if tk != "foobar" { + t.Error("Expected foobar, but got", tk) + } + +} + +func TestGetSessToken(t *testing.T) { + + myToken := &sessionToken{} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/login": + myChall := &challenge{ + Success: true, + } + myChall.Result.Challenge = "foobar" + result, _ := json.Marshal(myChall) + fmt.Fprintln(w, string(result)) + case "/session": + myToken.Success = true + myToken.Result.SessionToken = "foobar" + result, _ := json.Marshal(myToken) + fmt.Fprintln(w, string(result)) + case "/session2": + myToken.Msg = "failed to get a session" + myToken.Success = false + result, _ := json.Marshal(myToken) + fmt.Fprintln(w, string(result)) + default: + fmt.Fprintln(w, http.StatusNotFound) + } + })) + defer ts.Close() + + ai := authInfo{} + ai.myAPI.login = ts.URL + "/login" + ai.myAPI.loginSession = ts.URL + "/session" + var mySessionToken string + + st, err := getSessToken("token", &ai, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if st != "foobar" { + t.Error("Expected foobar, but got", st) + } + + ai.myAPI.loginSession = ts.URL + "/session2" + + _, err = getSessToken("token", &ai, &mySessionToken) + if err.Error() != "failed to get a session" { + t.Error("Expected but got failed to get a session, but got", err) + } +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..f65a39b --- /dev/null +++ b/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -eu + +ARCH=${1:-arm} +GOARCH=$ARCH go build -ldflags "-s -w" +ls -lh freebox_exporter +file freebox_exporter diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d393229 --- /dev/null +++ b/changelog.md @@ -0,0 +1,59 @@ +# Changelog + +## [1.3] - 2020-10-04 + +- Add VPN server metrics, mainly tx and rx for a user on a vpn with scr and local ip as labels + +## [1.2.2] - 2020-10-03 + +- Change variable type int to int64 for RRD metrics, causing an error "constant overflow" on arm chipset + +## [1.2] - 2020-05-27 + +- Add build script +- Log Freebox Server uptime and firmware version +- Log G.INP data +- Log connection status, protocol and modulation +- Log XDSL stats +- Don't log incorrect values (previously logged as zero) +- Remove dead code + +## [1.1.9] - 2020-04-24 + +- Log freeplug speeds and connectivity +- Go 1.14 +- Remove Godeps and vendored files + +## [1.1.7] - 2019-09-04 + +- Go 1.13 + +## [1.1.6] - 2019-09-04 + +- There is no more uncomfortable error message when the application renews its token +- Adding a `-fiber` flag so that Freebox fiber users do not capture DSL metrics, which are empty on this type of Freebox + +## [1.1.4] - 2019-09-03 + +- Adding a `-debug` flag to have more verbose error logs + +## [1.1.2] - 2019-08-25 + +- Improve error messages + +## [1.1.1] - 2019-07-31 + +- New Dockerfile for amd64 arch: reduce the image size to 3mb + +## [1.1.0] - 2019-07-31 + +- Fix temp metrics +- Add Godeps + +## [1.0.1] - 2019-07-30 + +- Change error catching + +## [1.0.0] - 2019-07-29 + +- Rewriting the application by adding a ton of unit tests diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 0000000..1c81599 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,9 @@ +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). + +Thanks to [mcanevet](https://gist.github.com/mcanevet) for his contribution: + +mcanevet Grafana dashbord + +Thanks to [Pichon](https://github.com/lepichon) for his contribution: + +lepichon Grafana dashbord \ No newline at end of file diff --git a/contrib/lepichon_freebox_grafana_dashboard.json b/contrib/lepichon_freebox_grafana_dashboard.json new file mode 100644 index 0000000..a018bea --- /dev/null +++ b/contrib/lepichon_freebox_grafana_dashboard.json @@ -0,0 +1,544 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "6.6.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "cacheTimeout": null, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 11, + "links": [], + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ], + "defaults": { + "mappings": [], + "max": 1, + "min": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(33, 33, 33)", + "value": null + } + ] + } + }, + "overrides": [], + "values": false + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "vertical" + }, + "pluginVersion": "6.6.0", + "repeat": null, + "targets": [ + { + "expr": "freebox_lan_reachable == 1", + "instant": true, + "legendFormat": "{{name}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Host on LAN", + "transparent": true, + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 3 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "/.*Trans.*/", + "transform": "negative-Y" + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(freebox_net_down_bytes[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Download Usage", + "refId": "A" + }, + { + "expr": "freebox_net_bw_down_bytes / 10", + "legendFormat": "Download Bandwidth", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network Traffic Download (bytes/sec)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "Bps", + "label": "Bytes out (-) / in (+)", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "/.*Trans.*/", + "transform": "negative-Y" + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(freebox_net_up_bytes[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Upload Usage", + "refId": "A" + }, + { + "expr": "freebox_net_bw_up_bytes / 10", + "legendFormat": "Upload Bandwidth", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network Traffic Upload (bytes/sec)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "Bps", + "label": "Bytes out (-) / in (+)", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "description": "", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 19 + }, + "hiddenSeries": false, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pluginVersion": "6.6.0", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "freebox_system_fan_rpm", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Fan Speed", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 3, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 19 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "freebox_system_temp_celsius", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "System Temperature", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "celsius", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "5s", + "schemaVersion": 22, + "style": "dark", + "tags": [ + "prometheus", + "freebox" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Freebox", + "uid": "TVsfEYmZk", + "version": 14 +} \ No newline at end of file diff --git a/contrib/mcanevet_freebox_grafana_dashboard.json b/contrib/mcanevet_freebox_grafana_dashboard.json new file mode 100644 index 0000000..c4e86e5 --- /dev/null +++ b/contrib/mcanevet_freebox_grafana_dashboard.json @@ -0,0 +1,418 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 5, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "/.*Trans.*/", + "transform": "negative-Y" + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(freebox_net_down_bytes[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Receive", + "refId": "B" + }, + { + "expr": "irate(freebox_net_up_bytes[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Transmit", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Network Traffic by bytes", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "Bps", + "label": "Bytes out (-) / in (+)", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "freebox_system_fan_rpm", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Fan RPM", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 6, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "freebox_system_temp_celsius", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "System Temperature", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "celsius", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 8, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(freebox_lan_reachable)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Hosts on LAN", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + } + ], + "refresh": "30s", + "schemaVersion": 18, + "style": "dark", + "tags": [ + "prometheus", + "freebox" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Freebox", + "uid": "TVsfEYmZk", + "version": 14 +} diff --git a/gauges.go b/gauges.go new file mode 100644 index 0000000..35d0e6c --- /dev/null +++ b/gauges.go @@ -0,0 +1,255 @@ +package main + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // XXX: see https://dev.freebox.fr/sdk/os/ for API documentation + // XXX: see https://prometheus.io/docs/practices/naming/ for metric names + + // connectionXdsl + connectionXdslStatusUptimeGauges = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_status_uptime_seconds_total", + }, + []string{ + "status", + "protocol", + "modulation", + }, + ) + + connectionXdslDownAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_down_attn_decibels", + }) + connectionXdslUpAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_up_attn_decibels", + }) + connectionXdslDownSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_down_snr_decibels", + }) + connectionXdslUpSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_up_snr_decibels", + }) + + connectionXdslErrorGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_errors_total", + Help: "Error counts", + }, + []string{ + "direction", // up|down + "name", // crc|es|fec|hec + }, + ) + + connectionXdslGinpGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_ginp", + }, + []string{ + "direction", // up|down + "name", // enabled|rtx_(tx|c|uc) + }, + ) + + connectionXdslNitroGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_connection_xdsl_nitro", + }, + []string{ + "direction", // up|down + }, + ) + + // RRD dsl [unstable] + rateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_dsl_up_bytes", + Help: "Available upload bandwidth (in byte/s)", + }) + rateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_dsl_down_bytes", + Help: "Available download bandwidth (in byte/s)", + }) + snrUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_dsl_snr_up_decibel", + Help: "Upload signal/noise ratio (in 1/10 dB)", + }) + snrDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_dsl_snr_down_decibel", + Help: "Download signal/noise ratio (in 1/10 dB)", + }) + + // freeplug + freeplugRxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "freebox_freeplug_rx_rate_bits", + Help: "rx rate (from the freeplugs to the \"cco\" freeplug) (in bits/s) -1 if not available", + }, + []string{ + "id", + }, + ) + freeplugTxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "freebox_freeplug_tx_rate_bits", + Help: "tx rate (from the \"cco\" freeplug to the freeplugs) (in bits/s) -1 if not available", + }, + []string{ + "id", + }, + ) + freeplugHasNetworkGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "freebox_freeplug_has_network", + Help: "is connected to the network", + }, + []string{ + "id", + }, + ) + + // RRD Net [unstable] + bwUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_net_bw_up_bytes", + Help: "Upload available bandwidth (in byte/s)", + }) + bwDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_net_bw_down_bytes", + Help: "Download available bandwidth (in byte/s)", + }) + netRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_net_up_bytes", + Help: "Upload rate (in byte/s)", + }) + netRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_net_down_bytes", + Help: "Download rate (in byte/s)", + }) + vpnRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_net_vpn_up_bytes", + Help: "Vpn client upload rate (in byte/s)", + }) + vpnRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "freebox_net_vpn_down_bytes", + Help: "Vpn client download rate (in byte/s)", + }) + + // Lan + lanReachableGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_lan_reachable", + Help: "Hosts reachable on LAN", + }, + []string{ + "name", // hostname + "vendor", + "ip", + }, + ) + + systemTempGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_system_temp_celsius", + Help: "Temperature sensors reported by system (in °C)", + }, + []string{ + "name", + }, + ) + + systemFanGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_system_fan_rpm", + Help: "Fan speed reported by system (in RPM)", + }, + []string{ + "name", + }, + ) + + systemUptimeGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_system_uptime_seconds_total", + }, + []string{ + "firmware_version", + }, + ) + + // wifi + wifiLabels = []string{ + "access_point", + "hostname", + "state", + } + + wifiSignalGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_signal_attenuation_db", + Help: "Wifi signal attenuation in decibel", + }, + wifiLabels, + ) + + wifiInactiveGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_inactive_duration_seconds", + Help: "Wifi inactive duration in seconds", + }, + wifiLabels, + ) + + wifiConnectionDurationGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_connection_duration_seconds", + Help: "Wifi connection duration in seconds", + }, + wifiLabels, + ) + + wifiRXBytesGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_rx_bytes", + Help: "Wifi received data (from station to Freebox) in bytes", + }, + wifiLabels, + ) + + wifiTXBytesGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_tx_bytes", + Help: "Wifi transmitted data (from Freebox to station) in bytes", + }, + wifiLabels, + ) + + wifiRXRateGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_rx_rate", + Help: "Wifi reception data rate (from station to Freebox) in bytes/seconds", + }, + wifiLabels, + ) + + wifiTXRateGauges = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "freebox_wifi_tx_rate", + Help: "Wifi transmission data rate (from Freebox to station) in bytes/seconds", + }, + wifiLabels, + ) + + // vpn server connections list [unstable] + vpnServerConnectionsList = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "vpn_server_connections_list", + Help: "VPN server connections list", + }, + []string{ + "user", + "vpn", + "src_ip", + "local_ip", + "name", // rx_bytes|tx_bytes + }, + ) +) diff --git a/getters.go b/getters.go new file mode 100644 index 0000000..9ad426e --- /dev/null +++ b/getters.go @@ -0,0 +1,573 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" +) + +var ( + apiErrors = map[string]error{ + "invalid_token": errors.New("The app token you are trying to use is invalid or has been revoked"), + "insufficient_rights": errors.New("Your app permissions does not allow accessing this API"), + "denied_from_external_ip": errors.New("You are trying to get an app_token from a remote IP"), + "invalid_request": errors.New("Your request is invalid"), + "ratelimited": errors.New("Too many auth error have been made from your IP"), + "new_apps_denied": errors.New("New application token request has been disabled"), + "apps_denied": errors.New("API access from apps has been disabled"), + "internal_error": errors.New("Internal error"), + "db_error": errors.New("Oops, the database you are trying to access doesn't seem to exist"), + "nodev": errors.New("Invalid interface"), + } +) + +func (r *rrd) status() error { + if apiErrors[r.ErrorCode] == nil { + return errors.New("RRD: The API returns an unknown error_code: " + r.ErrorCode) + } + return apiErrors[r.ErrorCode] +} + +func (l *lan) status() error { + if apiErrors[l.ErrorCode] == nil { + return errors.New("LAN: The API returns an unknown error_code: " + l.ErrorCode) + } + return apiErrors[l.ErrorCode] +} + +func setFreeboxToken(authInf *authInfo, xSessionToken *string) (string, error) { + token := os.Getenv("FREEBOX_TOKEN") + + if token == "" { + var err error + *xSessionToken, err = getToken(authInf, xSessionToken) + if err != nil { + return "", err + } + token = *xSessionToken + } + + if *xSessionToken == "" { + var err error + *xSessionToken, err = getSessToken(token, authInf, xSessionToken) + if err != nil { + log.Fatal(err) + } + token = *xSessionToken + } + + return token, nil + +} + +func newPostRequest() *postRequest { + return &postRequest{ + method: "POST", + url: mafreebox + "api/v4/rrd/", + header: "X-Fbx-App-Auth", + } +} + +func getConnectionXdsl(authInf *authInfo, pr *postRequest, xSessionToken *string) (connectionXdsl, error) { + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return connectionXdsl{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return connectionXdsl{}, err + } + if resp.StatusCode == 404 { + return connectionXdsl{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return connectionXdsl{}, err + } + + connectionXdslResp := connectionXdsl{} + err = json.Unmarshal(body, &connectionXdslResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return connectionXdsl{}, err + } + + return connectionXdslResp, nil +} + +func getDsl(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { + d := &database{ + DB: "dsl", + Fields: []string{"rate_up", "rate_down", "snr_up", "snr_down"}, + Precision: 10, + DateStart: int(time.Now().Unix() - 10), + } + + freeboxToken, err := setFreeboxToken(authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + client := http.Client{} + r, err := json.Marshal(*d) + if err != nil { + return []int64{}, err + } + buf := bytes.NewReader(r) + req, err := http.NewRequest(pr.method, pr.url, buf) + if err != nil { + return []int64{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return []int64{}, err + } + if resp.StatusCode == 404 { + return []int64{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []int64{}, err + } + rrdTest := rrd{} + err = json.Unmarshal(body, &rrdTest) + if err != nil { + if debug { + log.Println(string(body)) + } + return []int64{}, err + } + + if rrdTest.ErrorCode == "auth_required" { + *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + } + + if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { + if rrdTest.status().Error() == "Unknown return code from the API" { + fmt.Println("getDsl") + } + return []int64{}, rrdTest.status() + } + + if len(rrdTest.Result.Data) == 0 { + return []int64{}, nil + } + + 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"]} + return result, nil +} + +func getTemp(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { + d := &database{ + DB: "temp", + Fields: []string{"cpum", "cpub", "sw", "hdd", "fan_speed"}, + Precision: 10, + DateStart: int(time.Now().Unix() - 10), + } + + freeboxToken, err := setFreeboxToken(authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + + client := http.Client{} + r, err := json.Marshal(*d) + if err != nil { + return []int64{}, err + } + buf := bytes.NewReader(r) + req, err := http.NewRequest(pr.method, fmt.Sprintf(pr.url), buf) + if err != nil { + return []int64{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return []int64{}, err + } + if resp.StatusCode == 404 { + return []int64{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []int64{}, err + } + rrdTest := rrd{} + err = json.Unmarshal(body, &rrdTest) + if err != nil { + if debug { + log.Println(string(body)) + } + return []int64{}, err + } + + if rrdTest.ErrorCode == "auth_required" { + *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + } + + if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { + if rrdTest.status().Error() == "Unknown return code from the API" { + fmt.Println("getTemp") + } + return []int64{}, rrdTest.status() + } + + if len(rrdTest.Result.Data) == 0 { + return []int64{}, nil + } + + 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 +} + +func getNet(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { + d := &database{ + DB: "net", + Fields: []string{"bw_up", "bw_down", "rate_up", "rate_down", "vpn_rate_up", "vpn_rate_down"}, + Precision: 10, + DateStart: int(time.Now().Unix() - 10), + } + + freeboxToken, err := setFreeboxToken(authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + + client := http.Client{} + r, err := json.Marshal(*d) + if err != nil { + return []int64{}, err + } + buf := bytes.NewReader(r) + req, err := http.NewRequest(pr.method, pr.url, buf) + if err != nil { + return []int64{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return []int64{}, err + } + if resp.StatusCode == 404 { + return []int64{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []int64{}, err + } + rrdTest := rrd{} + err = json.Unmarshal(body, &rrdTest) + if err != nil { + if debug { + log.Println(string(body)) + } + return []int64{}, err + } + + if rrdTest.ErrorCode == "auth_required" { + *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + } + + if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { + if rrdTest.status().Error() == "Unknown return code from the API" { + fmt.Println("getNet") + } + return []int64{}, rrdTest.status() + } + + if len(rrdTest.Result.Data) == 0 { + return []int64{}, nil + } + + 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 +} + +func getSwitch(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) { + d := &database{ + DB: "switch", + Fields: []string{"rx_1", "tx_1", "rx_2", "tx_2", "rx_3", "tx_3", "rx_4", "tx_4"}, + Precision: 10, + DateStart: int(time.Now().Unix() - 10), + } + + freeboxToken, err := setFreeboxToken(authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + + client := http.Client{} + r, err := json.Marshal(*d) + if err != nil { + return []int64{}, err + } + buf := bytes.NewReader(r) + req, err := http.NewRequest(pr.method, pr.url, buf) + if err != nil { + return []int64{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return []int64{}, err + } + if resp.StatusCode == 404 { + return []int64{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []int64{}, err + } + rrdTest := rrd{} + err = json.Unmarshal(body, &rrdTest) + if err != nil { + if debug { + log.Println(string(body)) + } + return []int64{}, err + } + + if rrdTest.ErrorCode == "auth_required" { + *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) + if err != nil { + return []int64{}, err + } + } + + if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" { + if rrdTest.status().Error() == "Unknown return code from the API" { + fmt.Println("getSwitch") + } + return []int64{}, rrdTest.status() + } + + if len(rrdTest.Result.Data) == 0 { + return []int64{}, nil + } + + 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 +} + +func getLan(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]lanHost, error) { + freeboxToken, err := setFreeboxToken(authInf, xSessionToken) + if err != nil { + return []lanHost{}, err + } + + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return []lanHost{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return []lanHost{}, err + } + if resp.StatusCode == 404 { + return []lanHost{}, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []lanHost{}, err + } + + lanResp := lan{} + err = json.Unmarshal(body, &lanResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return []lanHost{}, err + } + + if lanResp.ErrorCode == "auth_required" { + *xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken) + if err != nil { + return []lanHost{}, err + } + } + + if lanResp.ErrorCode != "" && lanResp.ErrorCode != "auth_required" { + return []lanHost{}, lanResp.status() + } + + return lanResp.Result, nil +} + +func getFreeplug(authInf *authInfo, pr *postRequest, xSessionToken *string) (freeplug, error) { + if _, err := setFreeboxToken(authInf, xSessionToken); err != nil { + return freeplug{}, err + } + + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return freeplug{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return freeplug{}, err + } + if resp.StatusCode == 404 { + return freeplug{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return freeplug{}, err + } + + freeplugResp := freeplug{} + err = json.Unmarshal(body, &freeplugResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return freeplug{}, err + } + + return freeplugResp, nil +} + +func getSystem(authInf *authInfo, pr *postRequest, xSessionToken *string) (system, error) { + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return system{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return system{}, err + } + if resp.StatusCode == 404 { + return system{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return system{}, err + } + + systemResp := system{} + err = json.Unmarshal(body, &systemResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return system{}, err + } + + return systemResp, nil +} + +func getWifi(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifi, error) { + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return wifi{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return wifi{}, err + } + if resp.StatusCode == 404 { + return wifi{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return wifi{}, err + } + + wifiResp := wifi{} + err = json.Unmarshal(body, &wifiResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return wifi{}, err + } + + return wifiResp, nil +} + +func getWifiStations(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifiStations, error) { + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return wifiStations{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return wifiStations{}, err + } + if resp.StatusCode == 404 { + return wifiStations{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return wifiStations{}, err + } + + wifiStationResp := wifiStations{} + err = json.Unmarshal(body, &wifiStationResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return wifiStations{}, err + } + + return wifiStationResp, nil +} + +func getVpnServer(authInf *authInfo, pr *postRequest, xSessionToken *string) (vpnServer, error) { + client := http.Client{} + req, err := http.NewRequest(pr.method, pr.url, nil) + if err != nil { + return vpnServer{}, err + } + req.Header.Add(pr.header, *xSessionToken) + resp, err := client.Do(req) + if err != nil { + return vpnServer{}, err + } + if resp.StatusCode == 404 { + return vpnServer{}, errors.New(resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return vpnServer{}, err + } + + vpnServerResp := vpnServer{} + err = json.Unmarshal(body, &vpnServerResp) + if err != nil { + if debug { + log.Println(string(body)) + } + return vpnServer{}, err + } + + return vpnServerResp, nil +} diff --git a/getters_test.go b/getters_test.go new file mode 100644 index 0000000..7165f49 --- /dev/null +++ b/getters_test.go @@ -0,0 +1,727 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "reflect" + "strings" + "testing" +) + +func TestSetFreeboxToken(t *testing.T) { + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/login": + myChall := &challenge{ + Success: true, + } + myChall.Result.Challenge = "foobar" + result, _ := json.Marshal(myChall) + fmt.Fprintln(w, string(result)) + case "/session": + myToken := sessionToken{ + Success: true, + } + myToken.Result.SessionToken = "foobar" + result, _ := json.Marshal(myToken) + fmt.Fprintln(w, string(result)) + case "/granted/": + myTrack := track{ + Success: true, + } + myTrack.Result.TrackID = 101 + result, _ := json.Marshal(myTrack) + fmt.Fprintln(w, string(result)) + case "/granted/101": + myGrant := grant{ + Success: true, + } + myGrant.Result.Status = "granted" + result, _ := json.Marshal(myGrant) + fmt.Fprintln(w, string(result)) + default: + fmt.Fprintln(w, http.StatusNotFound) + } + })) + defer ts.Close() + + ai := &authInfo{} + ai.myStore.location = "/tmp/token" + ai.myAPI.login = ts.URL + "/login" + ai.myAPI.loginSession = ts.URL + "/session" + ai.myAPI.authz = ts.URL + "/granted/" + ai.myReader = bufio.NewReader(strings.NewReader("\n")) + + var mySessionToken string + + token, err := setFreeboxToken(ai, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + defer os.Remove(ai.myStore.location) + + if token != "foobar" { + t.Error("Expected foobar, but got", token) + } + + os.Setenv("FREEBOX_TOKEN", "barfoo") + defer os.Unsetenv("FREEBOX_TOKEN") + + token, err = setFreeboxToken(ai, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if token != "barfoo" { + t.Error("Expected barfoo, but got", token) + } +} + +func TestGetDsl(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/good": + myRRD := rrd{ + Success: true, + } + myRRD.Result.Data = []map[string]int64{ + { + "rate_up": 12, + "rate_down": 34, + "snr_up": 56, + "snr_down": 78, + }, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/error": + myRRD := rrd{ + Success: true, + ErrorCode: "insufficient_rights", + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/null": + myRRD := rrd{ + Success: true, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + } + })) + defer ts.Close() + + goodPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/good", + } + + errorPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/error", + } + + nullPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/null", + } + + ai := &authInfo{} + mySessionToken := "foobar" + + getDslResult, err := getDsl(ai, goodPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if getDslResult[0] != 12 || getDslResult[1] != 34 || getDslResult[2] != 56 || getDslResult[3] != 78 { + t.Errorf("Expected 12 34 56 78, but got %v %v %v %v\n", getDslResult[0], getDslResult[1], getDslResult[2], getDslResult[3]) + } + + getDslResult, err = getDsl(ai, errorPR, &mySessionToken) + if err.Error() != "Your app permissions does not allow accessing this API" { + t.Error("Expected Your app permissions does not allow accessing this API, but go", err) + } + + if len(getDslResult) != 0 { + t.Error("Expected 0, but got", len(getDslResult)) + } + + getDslResult, err = getDsl(ai, nullPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if len(getDslResult) != 0 { + t.Error("Expected 0, but got", len(getDslResult)) + } + +} + +func TestGetTemp(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/good": + myRRD := rrd{ + Success: true, + } + myRRD.Result.Data = []map[string]int64{ + { + "cpum": 01, + "cpub": 02, + "sw": 03, + "hdd": 04, + "fan_speed": 05, + }, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/error": + myRRD := rrd{ + Success: true, + ErrorCode: "denied_from_external_ip", + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/null": + myRRD := rrd{ + Success: true, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + } + })) + defer ts.Close() + + goodPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/good", + } + + errorPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/error", + } + + nullPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/null", + } + + ai := &authInfo{} + mySessionToken := "foobar" + + getTempResult, err := getTemp(ai, goodPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if getTempResult[0] != 01 || getTempResult[1] != 02 || getTempResult[2] != 03 || getTempResult[3] != 04 || getTempResult[4] != 05 { + 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]) + } + + getTempResult, err = getTemp(ai, errorPR, &mySessionToken) + if err.Error() != "You are trying to get an app_token from a remote IP" { + t.Error("Expected You are trying to get an app_token from a remote IP, but go", err) + } + + if len(getTempResult) != 0 { + t.Error("Expected 0, but got", len(getTempResult)) + } + + getTempResult, err = getTemp(ai, nullPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if len(getTempResult) != 0 { + t.Error("Expected 0, but got", len(getTempResult)) + } + +} + +func TestGetNet(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/good": + myRRD := rrd{ + Success: true, + } + myRRD.Result.Data = []map[string]int64{ + { + "bw_up": 12500000000, + "bw_down": 12500000000, + "rate_up": 12500000000, + "rate_down": 12500000000, + "vpn_rate_up": 12500000000, + "vpn_rate_down": 12500000000, + }, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/error": + myRRD := rrd{ + Success: true, + ErrorCode: "new_apps_denied", + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/null": + myRRD := rrd{ + Success: true, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + } + })) + defer ts.Close() + + goodPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/good", + } + + errorPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/error", + } + + nullPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/null", + } + + ai := &authInfo{} + mySessionToken := "foobar" + + getNetResult, err := getNet(ai, goodPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but go", err) + } + + if getNetResult[0] != 12500000000 || getNetResult[1] != 12500000000 || getNetResult[2] != 12500000000 || getNetResult[3] != 12500000000 || getNetResult[4] != 12500000000 || getNetResult[5] != 12500000000 { + 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]) + } + + getNetResult, err = getNet(ai, errorPR, &mySessionToken) + if err.Error() != "New application token request has been disabled" { + t.Error("Expected New application token request has been disabled, but got", err) + } + + if len(getNetResult) != 0 { + t.Error("Expected 0, but got", len(getNetResult)) + } + + getNetResult, err = getNet(ai, nullPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if len(getNetResult) != 0 { + t.Error("Expected 0, but got", len(getNetResult)) + } + +} + +func TestGetSwitch(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/good": + myRRD := rrd{ + Success: true, + } + myRRD.Result.Data = []map[string]int64{ + { + "rx_1": 01, + "tx_1": 11, + "rx_2": 02, + "tx_2": 12, + "rx_3": 03, + "tx_3": 13, + "rx_4": 04, + "tx_4": 14, + }, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/error": + myRRD := rrd{ + Success: true, + ErrorCode: "apps_denied", + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + case "/null": + myRRD := rrd{ + Success: true, + } + result, _ := json.Marshal(myRRD) + fmt.Fprintln(w, string(result)) + } + })) + defer ts.Close() + + goodPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/good", + } + + errorPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/error", + } + + nullPR := &postRequest{ + method: "POST", + header: "X-Fbx-App-Auth", + url: ts.URL + "/null", + } + + ai := &authInfo{} + mySessionToken := "foobar" + + getSwitchResult, err := getSwitch(ai, goodPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + 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 { + 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]) + } + + getSwitchResult, err = getSwitch(ai, errorPR, &mySessionToken) + if err.Error() != "API access from apps has been disabled" { + t.Error("Expected API access from apps has been disabled, but got", err) + } + + if len(getSwitchResult) != 0 { + t.Error("Expected 0, but got", len(getSwitchResult)) + } + + getSwitchResult, err = getSwitch(ai, nullPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if len(getSwitchResult) != 0 { + t.Error("Expected 0, but got", len(getSwitchResult)) + } + +} + +func TestGetLan(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/good": + myLan := lan{ + Success: true, + } + myLan.Result = []lanHost{ + { + Reachable: true, + PrimaryName: "Reachable host", + }, + { + Reachable: false, + PrimaryName: "Unreachable host", + }, + } + result, _ := json.Marshal(myLan) + fmt.Fprintln(w, string(result)) + case "/error": + myLan := lan{ + Success: true, + ErrorCode: "ratelimited", + } + result, _ := json.Marshal(myLan) + fmt.Fprintln(w, string(result)) + } + })) + defer ts.Close() + + goodPR := &postRequest{ + method: "GET", + header: "X-Fbx-App-Auth", + url: ts.URL + "/good", + } + + errorPR := &postRequest{ + method: "GET", + header: "X-Fbx-App-Auth", + url: ts.URL + "/error", + } + + ai := &authInfo{} + mySessionToken := "foobar" + + lanAvailable, err := getLan(ai, goodPR, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + for _, v := range lanAvailable { + if v.Reachable && v.PrimaryName != "Reachable host" { + t.Errorf("Expected Reachable: true, Host: Reachable host, but go Reachable: %v, Host: %v", v.Reachable, v.PrimaryName) + } + + if !v.Reachable && v.PrimaryName != "Unreachable host" { + t.Errorf("Expected Reachable: false, Host: Unreachable host, but go Reachable: %v, Host: %v", !v.Reachable, v.PrimaryName) + } + } + + lanAvailable, err = getLan(ai, errorPR, &mySessionToken) + if err.Error() != "Too many auth error have been made from your IP" { + t.Error("Expected Too many auth error have been made from your IP, but got", err) + } + +} + +func TestGetSystem(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mySys := system{ + Success: true, + } + mySys.Result.FanRPM = 666 + mySys.Result.TempCpub = 81 + mySys.Result.TempCpum = 89 + mySys.Result.TempHDD = 30 + mySys.Result.TempSW = 54 + + /* + mySys.Result { + FanRPM: 666, + TempCpub: 81, + TempCpum: 89, + TempHDD: 30, + TempSW: 54, + } + */ + result, _ := json.Marshal(mySys) + fmt.Fprintln(w, string(result)) + })) + defer ts.Close() + + pr := &postRequest{ + method: "GET", + header: "X-Fbx-App-Auth", + url: ts.URL, + } + + ai := &authInfo{} + mySessionToken := "foobar" + + systemStats, err := getSystem(ai, pr, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if systemStats.Result.FanRPM != 666 { + t.Error("Expected 666, but got", systemStats.Result.FanRPM) + } + + if systemStats.Result.TempCpub != 81 { + t.Error("Expected 81, but got", systemStats.Result.TempCpub) + } + + if systemStats.Result.TempCpum != 89 { + t.Error("Expected 89, but got", systemStats.Result.TempCpum) + } + + if systemStats.Result.TempHDD != 30 { + t.Error("Expected 30, but got", systemStats.Result.TempHDD) + } + + if systemStats.Result.TempSW != 54 { + t.Error("Expected 54, but got", systemStats.Result.TempSW) + } + +} + +func TestGetWifi(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + myWifi := wifi{ + Success: true, + } + myAP := wifiAccessPoint{ + Name: "AP1", + ID: 0, + } + myWifi.Result = []wifiAccessPoint{myAP} + + result, _ := json.Marshal(myWifi) + fmt.Fprintln(w, string(result)) + })) + defer ts.Close() + + pr := &postRequest{ + method: "GET", + header: "X-Fbx-App-Auth", + url: ts.URL, + } + + ai := &authInfo{} + mySessionToken := "foobar" + + wifiStats, err := getWifi(ai, pr, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if wifiStats.Result[0].Name != "AP1" { + t.Error("Expected AP1, but got", wifiStats.Result[0].Name) + } + + if wifiStats.Result[0].ID != 0 { + t.Error("Expected 0, but got", wifiStats.Result[0].ID) + } + +} + +func TestGetWifiStations(t *testing.T) { + os.Setenv("FREEBOX_TOKEN", "IOI") + defer os.Unsetenv("FREEBOX_TOKEN") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + myWifiStations := wifiStations{ + Success: true, + } + + myStation := wifiStation{ + Hostname: "station_host", + MAC: "AA:BB:CC:DD:EE:FF", + State: "authorized", + Inactive: 60, + RXBytes: 500, + TXBytes: 2280000000, + ConnectionDuration: 600, + TXRate: 4260000000, + RXRate: 5, + Signal: -20, + } + myWifiStations.Result = []wifiStation{myStation} + + result, _ := json.Marshal(myWifiStations) + fmt.Fprintln(w, string(result)) + })) + defer ts.Close() + + pr := &postRequest{ + method: "GET", + header: "X-Fbx-App-Auth", + url: ts.URL, + } + + ai := &authInfo{} + mySessionToken := "foobar" + + wifiStationsStats, err := getWifiStations(ai, pr, &mySessionToken) + if err != nil { + t.Error("Expected no err, but got", err) + } + + if wifiStationsStats.Result[0].Hostname != "station_host" { + t.Error("Expected station_host, but got", wifiStationsStats.Result[0].Hostname) + } + + if wifiStationsStats.Result[0].MAC != "AA:BB:CC:DD:EE:FF" { + t.Error("Expected AA:BB:CC:DD:EE:FF, but got", wifiStationsStats.Result[0].MAC) + } + + if wifiStationsStats.Result[0].State != "authorized" { + t.Error("Expected authorized, but got", wifiStationsStats.Result[0].State) + } + + if wifiStationsStats.Result[0].Inactive != 60 { + t.Error("Expected 60, but got", wifiStationsStats.Result[0].Inactive) + } + + if wifiStationsStats.Result[0].RXBytes != 500 { + t.Error("Expected 500, but got", wifiStationsStats.Result[0].RXBytes) + } + + if wifiStationsStats.Result[0].TXBytes != 10000 { + t.Error("Expected 10000, but got", wifiStationsStats.Result[0].TXBytes) + } + + if wifiStationsStats.Result[0].ConnectionDuration != 600 { + t.Error("Expected 600, but got", wifiStationsStats.Result[0].ConnectionDuration) + } + + if wifiStationsStats.Result[0].TXRate != 20 { + t.Error("Expected 20, but got", wifiStationsStats.Result[0].TXRate) + } + + if wifiStationsStats.Result[0].RXRate != 5 { + t.Error("Expected 5, but got", wifiStationsStats.Result[0].RXRate) + } + + if wifiStationsStats.Result[0].Signal != -20 { + t.Error("Expected -20, but got", wifiStationsStats.Result[0].Signal) + } + +} + +func Test_getNet(t *testing.T) { + type args struct { + authInf *authInfo + pr *postRequest + xSessionToken *string + } + tests := []struct { + name string + args args + want []int + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getNet(tt.args.authInf, tt.args.pr, tt.args.xSessionToken) + if (err != nil) != tt.wantErr { + t.Errorf("getNet() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getNet() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..79db6ea --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module freebox_exporter + +go 1.14 + +require ( + github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c // indirect + github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 + github.com/prometheus/client_golang v0.9.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b23a1e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c h1:fQ4P1oAipLwec/j5tfZTYV/e5i9ICSk23uVL+TK9III= +github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= +github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3e82930 --- /dev/null +++ b/main.go @@ -0,0 +1,310 @@ +package main + +import ( + "bufio" + "flag" + "log" + "net/http" + "os" + "reflect" + "strconv" + "strings" + "time" + + "github.com/iancoleman/strcase" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + mafreebox string + listen string + debug bool + fiber bool +) + +func init() { + flag.StringVar(&mafreebox, "endpoint", "http://mafreebox.freebox.fr/", "Endpoint for freebox API") + flag.StringVar(&listen, "listen", ":10001", "Prometheus metrics port") + flag.BoolVar(&debug, "debug", false, "Debug mode") + flag.BoolVar(&fiber, "fiber", false, "Turn on if you're using a fiber Freebox") +} + +func main() { + flag.Parse() + + if !strings.HasSuffix(mafreebox, "/") { + mafreebox = mafreebox + "/" + } + + endpoint := mafreebox + "api/v4/login/" + myAuthInfo := &authInfo{ + myAPI: api{ + login: endpoint, + authz: endpoint + "authorize/", + loginSession: endpoint + "session/", + }, + myStore: store{location: os.Getenv("HOME") + "/.freebox_token"}, + myApp: app{ + AppID: "fr.freebox.exporter", + AppName: "prometheus-exporter", + AppVersion: "0.4", + DeviceName: "local", + }, + myReader: bufio.NewReader(os.Stdin), + } + + myPostRequest := newPostRequest() + + myConnectionXdslRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v4/connection/xdsl/", + header: "X-Fbx-App-Auth", + } + + myFreeplugRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v4/freeplug/", + header: "X-Fbx-App-Auth", + } + + myLanRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v4/lan/browser/pub/", + header: "X-Fbx-App-Auth", + } + + mySystemRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v4/system/", + header: "X-Fbx-App-Auth", + } + + myWifiRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v2/wifi/ap/", + header: "X-Fbx-App-Auth", + } + + myVpnRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v4/vpn/connection/", + header: "X-Fbx-App-Auth", + } + + var mySessionToken string + + go func() { + for { + // There is no DSL metric on fiber Freebox + // If you use a fiber Freebox, use -fiber flag to turn off this metric + if !fiber { + // connectionXdsl metrics + connectionXdslStats, err := getConnectionXdsl(myAuthInfo, myConnectionXdslRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with connectionXdsl metrics: %v", err) + } + + if connectionXdslStats.Success { + status := connectionXdslStats.Result.Status + result := connectionXdslStats.Result + down := result.Down + up := result.Up + + connectionXdslStatusUptimeGauges. + WithLabelValues(status.Status, status.Protocol, status.Modulation). + Set(float64(status.Uptime)) + + connectionXdslDownAttnGauge.Set(float64(down.Attn10) / 10) + connectionXdslUpAttnGauge.Set(float64(up.Attn10) / 10) + + // XXX: sometimes the Freebox is reporting zero as SNR which + // does not make sense so we don't log these + if down.Snr10 > 0 { + connectionXdslDownSnrGauge.Set(float64(down.Snr10) / 10) + } + if up.Snr10 > 0 { + connectionXdslUpSnrGauge.Set(float64(up.Snr10) / 10) + } + + connectionXdslNitroGauges.WithLabelValues("down"). + Set(bool2float(down.Nitro)) + connectionXdslNitroGauges.WithLabelValues("up"). + Set(bool2float(up.Nitro)) + + connectionXdslGinpGauges.WithLabelValues("down", "enabled"). + Set(bool2float(down.Ginp)) + connectionXdslGinpGauges.WithLabelValues("up", "enabled"). + Set(bool2float(up.Ginp)) + + logFields(result, connectionXdslGinpGauges, + []string{"rtx_tx", "rtx_c", "rtx_uc"}) + + logFields(result, connectionXdslErrorGauges, + []string{"crc", "es", "fec", "hec", "ses"}) + } + + // dsl metrics + getDslResult, err := getDsl(myAuthInfo, myPostRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with DSL metrics: %v", err) + } + + if len(getDslResult) > 0 { + rateUpGauge.Set(float64(getDslResult[0])) + rateDownGauge.Set(float64(getDslResult[1])) + snrUpGauge.Set(float64(getDslResult[2])) + snrDownGauge.Set(float64(getDslResult[3])) + } + } + + // freeplug metrics + freeplugStats, err := getFreeplug(myAuthInfo, myFreeplugRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with freeplug metrics: %v", err) + } + + for _, freeplugNetwork := range freeplugStats.Result { + for _, freeplugMember := range freeplugNetwork.Members { + if freeplugMember.HasNetwork { + freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(1)) + } else { + freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(0)) + } + + Mb := 1e6 + rxRate := float64(freeplugMember.RxRate) * Mb + txRate := float64(freeplugMember.TxRate) * Mb + + if rxRate >= 0 { // -1 if not unavailable + freeplugRxRateGauge.WithLabelValues(freeplugMember.ID).Set(rxRate) + } + + if txRate >= 0 { // -1 if not unavailable + freeplugTxRateGauge.WithLabelValues(freeplugMember.ID).Set(txRate) + } + } + } + + // net metrics + getNetResult, err := getNet(myAuthInfo, myPostRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with NET metrics: %v", err) + } + + if len(getNetResult) > 0 { + bwUpGauge.Set(float64(getNetResult[0])) + bwDownGauge.Set(float64(getNetResult[1])) + netRateUpGauge.Set(float64(getNetResult[2])) + netRateDownGauge.Set(float64(getNetResult[3])) + vpnRateUpGauge.Set(float64(getNetResult[4])) + vpnRateDownGauge.Set(float64(getNetResult[5])) + } + + // lan metrics + lanAvailable, err := getLan(myAuthInfo, myLanRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with LAN metrics: %v", err) + } + for _, v := range lanAvailable { + var Ip string + if len(v.L3c) > 0 { + Ip = v.L3c[0].Addr + } else { + Ip = "" + } + if v.Reachable { + lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(1)) + } else { + lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(0)) + } + } + + // system metrics + systemStats, err := getSystem(myAuthInfo, mySystemRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with System metrics: %v", err) + } + + systemTempGauges.WithLabelValues("Température CPU B").Set(float64(systemStats.Result.TempCpub)) + systemTempGauges.WithLabelValues("Température CPU M").Set(float64(systemStats.Result.TempCpum)) + systemTempGauges.WithLabelValues("Température Switch").Set(float64(systemStats.Result.TempSW)) + systemTempGauges.WithLabelValues("Disque dur").Set(float64(systemStats.Result.TempHDD)) + systemFanGauges.WithLabelValues("Ventilateur 1").Set(float64(systemStats.Result.FanRPM)) + + systemUptimeGauges. + WithLabelValues(systemStats.Result.FirmwareVersion). + Set(float64(systemStats.Result.UptimeVal)) + + // wifi metrics + wifiStats, err := getWifi(myAuthInfo, myWifiRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with Wifi metrics: %v", err) + } + for _, accessPoint := range wifiStats.Result { + myWifiStationRequest := &postRequest{ + method: "GET", + url: mafreebox + "api/v2/wifi/ap/" + strconv.Itoa(accessPoint.ID) + "/stations", + header: "X-Fbx-App-Auth", + } + wifiStationsStats, err := getWifiStations(myAuthInfo, myWifiStationRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with Wifi station metrics: %v", err) + } + for _, station := range wifiStationsStats.Result { + wifiSignalGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Signal)) + wifiInactiveGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Inactive)) + wifiConnectionDurationGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.ConnectionDuration)) + wifiRXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXBytes)) + wifiTXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXBytes)) + wifiRXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXRate)) + wifiTXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXRate)) + } + } + + // VPN Server Connections List + getVpnServerResult, err := getVpnServer(myAuthInfo, myVpnRequest, &mySessionToken) + if err != nil { + log.Printf("An error occured with VPN station metrics: %v", err) + } + for _, connection := range getVpnServerResult.Result { + 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)) + 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)) + } + + time.Sleep(10 * time.Second) + } + }() + + log.Println("freebox_exporter started on port", listen) + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(listen, nil)) +} + +func logFields(result interface{}, gauge *prometheus.GaugeVec, fields []string) error { + resultReflect := reflect.ValueOf(result) + + for _, direction := range []string{"down", "up"} { + for _, field := range fields { + value := reflect.Indirect(resultReflect). + FieldByName(strcase.ToCamel(direction)). + FieldByName(strcase.ToCamel(field)) + + if value.IsZero() { + continue + } + + gauge.WithLabelValues(direction, field). + Set(float64(value.Int())) + } + } + + return nil +} + +func bool2float(b bool) float64 { + if b { + return 1 + } + return 0 +} diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..9f8e4bf --- /dev/null +++ b/structs.go @@ -0,0 +1,276 @@ +package main + +import "bufio" + +type track struct { + Success bool `json:"success"` + Result struct { + AppToken string `json:"app_token"` + TrackID int `json:"track_id"` + } `json:"result"` +} + +type grant struct { + Success bool `json:"success"` + Result struct { + Status string `json:"status"` + Challenge string `json:"challenge"` + } `json:"result"` +} + +type challenge struct { + Success bool `json:"success"` + Result struct { + LoggedIN bool `json:"logged_in,omitempty"` + Challenge string `json:"challenge"` + } `json:"result"` +} + +type session struct { + AppID string `json:"app_id"` + Password string `json:"password"` +} + +type sessionToken struct { + Msg string `json:"msg,omitempty"` + Success bool `json:"success"` + UID string `json:"uid,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + Result struct { + SessionToken string `json:"session_token,omitempty"` + Challenge string `json:"challenge"` + Permissions struct { + Settings bool `json:"settings,omitempty"` + Contacts bool `json:"contacts,omitempty"` + Calls bool `json:"calls,omitempty"` + Explorer bool `json:"explorer,omitempty"` + Downloader bool `json:"downloader,omitempty"` + Parental bool `json:"parental,omitempty"` + Pvr bool `json:"pvr,omitempty"` + Home bool `json:"home,omitempty"` + Camera bool `json:"camera,omitempty"` + } `json:"permissions,omitempty"` + } `json:"result"` +} + +type rrd struct { + UID string `json:"uid,omitempty"` + Success bool `json:"success"` + Msg string `json:"msg,omitempty"` + Result struct { + DateStart int `json:"date_start,omitempty"` + DateEnd int `json:"date_end,omitempty"` + Data []map[string]int64 `json:"data,omitempty"` + } `json:"result"` + ErrorCode string `json:"error_code"` +} + +// https://dev.freebox.fr/sdk/os/connection/ +type connectionXdsl struct { + Success bool `json:"success"` + Result struct { + Status struct { + Status string `json:"status"` + Modulation string `json:"modulation"` + Protocol string `json:"protocol"` + Uptime int `json:"uptime"` + } `json:"status"` + Down struct { + Attn int `json:"attn"` + Attn10 int `json:"attn_10"` + Crc int `json:"crc"` + Es int `json:"es"` + Fec int `json:"fec"` + Ginp bool `json:"ginp"` + Hec int `json:"hec"` + Maxrate uint64 `json:"maxrate"` + Nitro bool `json:"nitro"` + Phyr bool `json:"phyr"` + Rate int `json:"rate"` + RtxC int `json:"rtx_c,omitempty"` + RtxTx int `json:"rtx_tx,omitempty"` + RtxUc int `json:"rtx_uc,omitempty"` + Rxmt int `json:"rxmt"` + RxmtCorr int `json:"rxmt_corr"` + RxmtUncorr int `json:"rxmt_uncorr"` + Ses int `json:"ses"` + Snr int `json:"snr"` + Snr10 int `json:"snr_10"` + } `json:"down"` + Up struct { + Attn int `json:"attn"` + Attn10 int `json:"attn_10"` + Crc int `json:"crc"` + Es int `json:"es"` + Fec int `json:"fec"` + Ginp bool `json:"ginp"` + Hec int `json:"hec"` + Maxrate uint64 `json:"maxrate"` + Nitro bool `json:"nitro"` + Phyr bool `json:"phyr"` + Rate uint64 `json:"rate"` + RtxC int `json:"rtx_c,omitempty"` + RtxTx int `json:"rtx_tx,omitempty"` + RtxUc int `json:"rtx_uc,omitempty"` + Rxmt int `json:"rxmt"` + RxmtCorr int `json:"rxmt_corr"` + RxmtUncorr int `json:"rxmt_uncorr"` + Ses int `json:"ses"` + Snr int `json:"snr"` + Snr10 int `json:"snr_10"` + } `json:"up"` + } +} + +type database struct { + DB string `json:"db"` + DateStart int `json:"date_start,omitempty"` + DateEnd int `json:"date_end,omitempty"` + Precision int `json:"precision,omitempty"` + Fields []string `json:"fields"` +} + +// https://dev.freebox.fr/sdk/os/freeplug/ +type freeplug struct { + Success bool `json:"success"` + Result []freeplugNetwork `json:"result"` +} + +type freeplugNetwork struct { + ID string `json:"id"` + Members []freeplugMember `json:"members"` +} + +type freeplugMember struct { + ID string `json:"id"` + Local bool `json:"local"` + NetRole string `json:"net_role"` + EthPortStatus string `json:"eth_port_status"` + EthFullDuplex bool `json:"eth_full_duplex"` + HasNetwork bool `json:"has_network"` + EthSpeed int `json:"eth_speed"` + Inative int `json:"inactive"` + NetID string `json:"net_id"` + RxRate int64 `json:"rx_rate"` + TxRate int64 `json:"tx_rate"` + Model string `json:"model"` +} + +// https://dev.freebox.fr/sdk/os/lan/ +type l3c struct { + Addr string `json:"addr,omitempty"` +} + +type lanHost struct { + Reachable bool `json:"reachable,omitempty"` + PrimaryName string `json:"primary_name,omitempty"` + Vendor_name string `json:"vendor_name,omitempty"` + L3c []l3c `json:"l3connectivities,omitempty"` +} + +type lan struct { + Success bool `json:"success"` + Result []lanHost `json:"result"` + ErrorCode string `json:"error_code"` +} + +type idNameValue struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Value int `json:"value,omitempty"` +} + +// https://dev.freebox.fr/sdk/os/system/ +type system struct { + Success bool `json:"success"` + Result struct { + Mac string `json:"mac,omitempty"` + FanRPM int `json:"fan_rpm,omitempty"` + BoxFlavor string `json:"box_flavor,omitempty"` + TempCpub int `json:"temp_cpub,omitempty"` + TempCpum int `json:"temp_cpum,omitempty"` + DiskStatus string `json:"disk_status,omitempty"` + TempHDD int `json:"temp_hdd,omitempty"` + BoardName string `json:"board_name,omitempty"` + TempSW int `json:"temp_sw,omitempty"` + Uptime string `json:"uptime,omitempty"` + UptimeVal int `json:"uptime_val,omitempty"` + UserMainStorage string `json:"user_main_storage,omitempty"` + BoxAuthenticated bool `json:"box_authenticated,omitempty"` + Serial string `json:"serial,omitempty"` + FirmwareVersion string `json:"firmware_version,omitempty"` + } +} + +// https://dev.freebox.fr/sdk/os/wifi/ +type wifiAccessPoint struct { + Name string `json:"name,omitempty"` + ID int `json:"id,omitempty"` +} + +type wifi struct { + Success bool `json:"success"` + Result []wifiAccessPoint `json:"result,omitempty"` +} + +type wifiStation struct { + Hostname string `json:"hostname,omitempty"` + MAC string `json:"mac,omitempty"` + State string `json:"state,omitempty"` + Inactive int `json:"inactive,omitempty"` + RXBytes int64 `json:"rx_bytes,omitempty"` + TXBytes int64 `json:"tx_bytes,omitempty"` + ConnectionDuration int `json:"conn_duration,omitempty"` + TXRate int64 `json:"tx_rate,omitempty"` + RXRate int64 `json:"rx_rate,omitempty"` + Signal int `json:"signal,omitempty"` +} + +type wifiStations struct { + Success bool `json:"success"` + Result []wifiStation `json:"result,omitempty"` +} + +type app struct { + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppVersion string `json:"app_version"` + DeviceName string `json:"device_name"` +} + +type api struct { + authz string + login string + loginSession string +} + +type store struct { + location string +} + +type authInfo struct { + myApp app + myAPI api + myStore store + myReader *bufio.Reader +} + +type postRequest struct { + method, url, header string +} + +// https://dev.freebox.fr/sdk/os/vpn/ +type vpnServer struct { + Success bool `json:"success"` + Result []struct { + RxBytes int64 `json:"rx_bytes,omitempty"` + Authenticated bool `json:"authenticated,omitempty"` + TxBytes int64 `json:"tx_bytes,omitempty"` + User string `json:"user,omitempty"` + ID string `json:"id,omitempty"` + Vpn string `json:"vpn,omitempty"` + SrcIP string `json:"src_ip,omitempty"` + AuthTime int32 `json:"auth_time,omitempty"` + LocalIP string `json:"local_ip,omitempty"` + } `json:"result,omitempty"` +}