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