first commit

This commit is contained in:
2023-06-26 19:37:38 +02:00
commit fcc14326c2
22 changed files with 4334 additions and 0 deletions

36
.github/workflows/amd64.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: amd64
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile-amd64
push: true
tags: saphoooo/freebox-exporter
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

36
.github/workflows/armv7.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: armv7
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile-armv7
push: true
tags: saphoooo/freebox-exporter:armv7
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '44 5 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/freebox_exporter

19
Dockerfile-armv7 Normal file
View File

@ -0,0 +1,19 @@
FROM golang:1.14
WORKDIR /
COPY . .
RUN set -x && \
go get -d -v . && \
CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags "-w -s" -a -installsuffix cgo -o app .
FROM scratch
LABEL maintainer="stephane.beuret@gmail.com"
COPY --from=0 app /
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]

28
Dockerfile.amd64 Normal file
View File

@ -0,0 +1,28 @@
FROM golang:1.14
WORKDIR /
COPY . .
ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz /usr/local
RUN set -x && \
apt update && \
apt install -y xz-utils && \
xz -d -c /usr/local/upx-3.95-amd64_linux.tar.xz | \
tar -xOf - upx-3.95-amd64_linux/upx > /bin/upx && \
chmod a+x /bin/upx && \
go get -d -v . && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o app . && \
strip --strip-unneeded app && \
upx app
FROM scratch
LABEL maintainer="stephane.beuret@gmail.com"
COPY --from=0 app /
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

91
README.md Normal file
View File

@ -0,0 +1,91 @@
# freebox_exporter
A Prometheus exporter for Freebox stats
## Cmds
`freebox_exporter`
## Flags
- `-endpoint`: Freebox API url (default http://mafreebox.freebox.fr)
- `-listen`: port for Prometheus metrics (default :10001)
- `-debug`: turn on debug mode
- `-fiber`: turn off DSL metric for fiber Freebox
## Preview
Here's what you can get in Prometheus / Grafana with freebox_exporter:
![Preview](https://user-images.githubusercontent.com/13923756/54585380-33318800-4a1a-11e9-8e9d-e434f275755c.png)
# How to use it
## Compiled binary
If you want to compile the binary, you can refer to [this document](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63) which explains how to do it, depending on your OS and architecture. Alternatively, you can use `./build.sh`.
You can also find the compiled binaries for MacOS, Linux (x86_64, arm64 and arm) and Windows in the release tab.
### Quick start
```
./freebox_exporter
```
### The following parameters are optional and can be overridden:
- Freebox API endpoint
```
./freebox_exporter -endpoint "http://mafreebox.freebox.fr"
```
- Port
```
./freebox_exporter -listen ":10001"
```
## Docker
### Quick start
```
docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \
saphoooo/freebox-exporter
```
### The following parameters are optional and can be overridden:
- Local token
Volume allows to save the access token outside of the container to reuse authentication upon an update of the container.
```
docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \
-e HOME=token -v /path/to/token:/token saphoooo/freebox-exporter
```
- Freebox API endpoint
```
docker run -d --name freebox-exporter --restart on-failure -p 10001:10001
saphoooo/freebox-exporter -endpoint "http://mafreebox.freebox.fr"
```
- Port
```
docker run -d --name freebox-exporter --restart on-failure -p 8080:10001 \
saphoooo/freebox-exporter
```
## Caution on first run
If you launch the application for the first time, you must allow it to access the freebox API.
- The application must be launched from the local network.
- You have to authorize the application from the freebox front panel.
- You have to modify the rights of the application to give it "Modification des réglages de la Freebox"
Source: https://dev.freebox.fr/sdk/os/

227
authz.go Normal file
View File

@ -0,0 +1,227 @@
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"time"
)
// storeToken stores app_token in ~/.freebox_token
func storeToken(token string, authInf *authInfo) error {
err := os.Setenv("FREEBOX_TOKEN", token)
if err != nil {
return err
}
if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) {
err := ioutil.WriteFile(authInf.myStore.location, []byte(token), 0600)
if err != nil {
return err
}
}
return nil
}
// retreiveToken gets the token from file and
// load it in environment variable
func retreiveToken(authInf *authInfo) (string, error) {
if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) {
return "", err
}
data, err := ioutil.ReadFile(authInf.myStore.location)
if err != nil {
return "", err
}
err = os.Setenv("FREEBOX_TOKEN", string(data))
if err != nil {
return "", err
}
return string(data), nil
}
// getTrackID is the initial request to freebox API
// get app_token and track_id
func getTrackID(authInf *authInfo) (*track, error) {
req, _ := json.Marshal(authInf.myApp)
buf := bytes.NewReader(req)
resp, err := http.Post(authInf.myAPI.authz, "application/json", buf)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
trackID := track{}
err = json.Unmarshal(body, &trackID)
if err != nil {
return nil, err
}
err = storeToken(trackID.Result.AppToken, authInf)
if err != nil {
return nil, err
}
return &trackID, nil
}
// getGranted waits for user to validate on the freebox front panel
// with a timeout of 15 seconds
func getGranted(authInf *authInfo) error {
trackID, err := getTrackID(authInf)
if err != nil {
return err
}
url := authInf.myAPI.authz + strconv.Itoa(trackID.Result.TrackID)
for i := 0; i < 15; i++ {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
granted := grant{}
err = json.Unmarshal(body, &granted)
if err != nil {
return err
}
switch granted.Result.Status {
case "unknown":
return errors.New("the app_token is invalid or has been revoked")
case "pending":
log.Println("the user has not confirmed the authorization request yet")
case "timeout":
return errors.New("the user did not confirmed the authorization within the given time")
case "granted":
log.Println("the app_token is valid and can be used to open a session")
i = 15
case "denied":
return errors.New("the user denied the authorization request")
}
time.Sleep(1 * time.Second)
}
return nil
}
// getChallenge makes sure the app always has a valid challenge
func getChallenge(authInf *authInfo) (*challenge, error) {
resp, err := http.Get(authInf.myAPI.login)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
challenged := challenge{}
err = json.Unmarshal(body, &challenged)
if err != nil {
return nil, err
}
return &challenged, nil
}
// hmacSha1 encodes app_token in hmac-sha1 and stores it in password
func hmacSha1(appToken, challenge string) string {
hash := hmac.New(sha1.New, []byte(appToken))
hash.Write([]byte(challenge))
return hex.EncodeToString(hash.Sum(nil))
}
// getSession gets a session with freeebox API
func getSession(authInf *authInfo, passwd string) (*sessionToken, error) {
s := session{
AppID: authInf.myApp.AppID,
Password: passwd,
}
req, err := json.Marshal(s)
if err != nil {
return nil, err
}
buf := bytes.NewReader(req)
resp, err := http.Post(authInf.myAPI.loginSession, "application/json", buf)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
token := sessionToken{}
err = json.Unmarshal(body, &token)
if err != nil {
return nil, err
}
return &token, nil
}
// getToken gets a valid session_token and asks for user to change
// the set of permissions on the API
func getToken(authInf *authInfo, xSessionToken *string) (string, error) {
if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) {
err = getGranted(authInf)
if err != nil {
return "", err
}
reader := authInf.myReader
log.Println("check \"Modification des réglages de la Freebox\" and press enter")
_, err = reader.ReadString('\n')
if err != nil {
return "", err
}
} else {
_, err := retreiveToken(authInf)
if err != nil {
return "", err
}
}
token, err := getSessToken(os.Getenv("FREEBOX_TOKEN"), authInf, xSessionToken)
if err != nil {
return "", err
}
*xSessionToken = token
return token, nil
}
// getSessToken gets a new token session when the old one has expired
func getSessToken(token string, authInf *authInfo, xSessionToken *string) (string, error) {
challenge, err := getChallenge(authInf)
if err != nil {
return "", err
}
password := hmacSha1(token, challenge.Result.Challenge)
t, err := getSession(authInf, password)
if err != nil {
return "", err
}
if t.Success == false {
return "", errors.New(t.Msg)
}
*xSessionToken = t.Result.SessionToken
return t.Result.SessionToken, nil
}

413
authz_test.go Normal file
View File

@ -0,0 +1,413 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
func TestRetreiveToken(t *testing.T) {
ai := &authInfo{
myStore: store{location: "/tmp/token"},
}
_, err := retreiveToken(ai)
if err.Error() != "stat /tmp/token: no such file or directory" {
t.Error("Expected bla, but got", err)
}
ioutil.WriteFile(ai.myStore.location, []byte("IOI"), 0600)
defer os.Remove(ai.myStore.location)
token, err := retreiveToken(ai)
if err != nil {
t.Error("Expected no err, but got", err)
}
newToken := os.Getenv("FREEBOX_TOKEN")
if newToken != "IOI" {
t.Error("Expected IOI, but got", newToken)
}
if token != "IOI" {
t.Error("Expected IOI, but got", newToken)
}
os.Unsetenv("FREEBOX_TOKEN")
}
func TestStoreToken(t *testing.T) {
var token string
ai := &authInfo{}
token = "IOI"
err := storeToken(token, ai)
if err.Error() != "open : no such file or directory" {
t.Error("Expected open : no such file or directory, but got", err)
}
ai.myStore.location = "/tmp/token"
err = storeToken(token, ai)
if err != nil {
t.Error("Expected no err, but got", err)
}
defer os.Remove(ai.myStore.location)
token = os.Getenv("FREEBOX_TOKEN")
if token != "IOI" {
t.Error("Expected IOI, but got", token)
}
os.Unsetenv("FREEBOX_TOKEN")
data, err := ioutil.ReadFile(ai.myStore.location)
if err != nil {
t.Error("Expected no err, but got", err)
}
if string(data) != "IOI" {
t.Error("Expected IOI, but got", string(data))
}
}
func TestGetTrackID(t *testing.T) {
ai := &authInfo{
myStore: store{location: "/tmp/token"},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
myTrack := track{
Success: true,
}
myTrack.Result.AppToken = "IOI"
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
}))
defer ts.Close()
ai.myAPI.authz = ts.URL
trackID, err := getTrackID(ai)
if err != nil {
t.Error("Expected no err, but got", err)
}
defer os.Remove(ai.myStore.location)
defer os.Unsetenv("FREEBOX_TOKEN")
if trackID.Result.TrackID != 101 {
t.Error("Expected 101, but got", trackID.Result.TrackID)
}
// as getTrackID have no return value
// the result of storeToken func is checked instead
token := os.Getenv("FREEBOX_TOKEN")
if token != "IOI" {
t.Error("Expected IOI, but got", token)
}
}
func TestGetGranted(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/unknown/":
myTrack := track{
Success: true,
}
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
case "/unknown/101":
myGrant := grant{
Success: true,
}
myGrant.Result.Status = "unknown"
result, _ := json.Marshal(myGrant)
fmt.Fprintln(w, string(result))
case "/timeout/":
myTrack := track{
Success: true,
}
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
case "/timeout/101":
myGrant := grant{
Success: true,
}
myGrant.Result.Status = "timeout"
result, _ := json.Marshal(myGrant)
fmt.Fprintln(w, string(result))
case "/denied/":
myTrack := track{
Success: true,
}
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
case "/denied/101":
myGrant := grant{
Success: true,
}
myGrant.Result.Status = "denied"
result, _ := json.Marshal(myGrant)
fmt.Fprintln(w, string(result))
case "/granted/":
myTrack := track{
Success: true,
}
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
case "/granted/101":
myGrant := grant{
Success: true,
}
myGrant.Result.Status = "granted"
result, _ := json.Marshal(myGrant)
fmt.Fprintln(w, string(result))
default:
fmt.Fprintln(w, http.StatusNotFound)
}
}))
defer ts.Close()
ai := authInfo{}
ai.myAPI.authz = ts.URL + "/unknown/"
ai.myStore.location = "/tmp/token"
err := getGranted(&ai)
if err.Error() != "the app_token is invalid or has been revoked" {
t.Error("Expected the app_token is invalid or has been revoked, but got", err)
}
defer os.Remove(ai.myStore.location)
defer os.Unsetenv("FREEBOX_TOKEN")
ai.myAPI.authz = ts.URL + "/timeout/"
err = getGranted(&ai)
if err.Error() != "the user did not confirmed the authorization within the given time" {
t.Error("Expected the user did not confirmed the authorization within the given time, but got", err)
}
ai.myAPI.authz = ts.URL + "/denied/"
err = getGranted(&ai)
if err.Error() != "the user denied the authorization request" {
t.Error("Expected the user denied the authorization request, but got", err)
}
ai.myAPI.authz = ts.URL + "/granted/"
err = getGranted(&ai)
if err != nil {
t.Error("Expected no err, but got", err)
}
}
func TestGetChallenge(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
myChall := &challenge{
Success: true,
}
myChall.Result.Challenge = "foobar"
result, _ := json.Marshal(myChall)
fmt.Fprintln(w, string(result))
}))
defer ts.Close()
ai := &authInfo{
myAPI: api{
login: ts.URL,
},
}
challenged, err := getChallenge(ai)
if err != nil {
t.Error("Expected no err, but got", err)
}
if challenged.Success != true {
t.Error("Expected true, but got", challenged.Success)
}
if challenged.Result.Challenge != "foobar" {
t.Error("Expected foobar, but got", challenged.Result.Challenge)
}
}
func TestHmacSha1(t *testing.T) {
hmac := hmacSha1("IOI", "foobar")
if hmac != "02fb876a39b64eddcfee3eaa69465cb3e8d53cde" {
t.Error("Expected 02fb876a39b64eddcfee3eaa69465cb3e8d53cde, but got", hmac)
}
}
func TestGetSession(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
myToken := &sessionToken{
Success: true,
}
myToken.Result.Challenge = "foobar"
result, _ := json.Marshal(myToken)
fmt.Fprintln(w, string(result))
}))
defer ts.Close()
ai := &authInfo{
myAPI: api{
loginSession: ts.URL,
},
}
token, err := getSession(ai, "")
if err != nil {
t.Error("Expected no err, but got", err)
}
defer os.Unsetenv("FREEBOX_TOKEN")
if token.Success != true {
t.Error("Expected true, but got", token.Success)
}
if token.Result.Challenge != "foobar" {
t.Error("Expected foobar, but got", token.Result.Challenge)
}
}
func TestGetToken(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/login":
myChall := &challenge{
Success: true,
}
myChall.Result.Challenge = "foobar"
result, _ := json.Marshal(myChall)
fmt.Fprintln(w, string(result))
case "/session":
myToken := sessionToken{
Success: true,
}
myToken.Result.SessionToken = "foobar"
result, _ := json.Marshal(myToken)
fmt.Fprintln(w, string(result))
case "/granted/":
myTrack := track{
Success: true,
}
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
case "/granted/101":
myGrant := grant{
Success: true,
}
myGrant.Result.Status = "granted"
result, _ := json.Marshal(myGrant)
fmt.Fprintln(w, string(result))
default:
fmt.Fprintln(w, http.StatusNotFound)
}
}))
defer ts.Close()
ai := authInfo{}
ai.myStore.location = "/tmp/token"
ai.myAPI.login = ts.URL + "/login"
ai.myAPI.loginSession = ts.URL + "/session"
ai.myAPI.authz = ts.URL + "/granted/"
ai.myReader = bufio.NewReader(strings.NewReader("\n"))
var mySessionToken string
// the first pass valide getToken without a token stored in a file
tk, err := getToken(&ai, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
defer os.Remove(ai.myStore.location)
defer os.Unsetenv("FREEBOX_TOKEN")
if mySessionToken != "foobar" {
t.Error("Expected foobar, but got", mySessionToken)
}
if tk != "foobar" {
t.Error("Expected foobar, but got", tk)
}
// the second pass validate getToken with a token stored in a file:
// the first pass creates a file at ai.myStore.location
tk, err = getToken(&ai, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if mySessionToken != "foobar" {
t.Error("Expected foobar, but got", mySessionToken)
}
if tk != "foobar" {
t.Error("Expected foobar, but got", tk)
}
}
func TestGetSessToken(t *testing.T) {
myToken := &sessionToken{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/login":
myChall := &challenge{
Success: true,
}
myChall.Result.Challenge = "foobar"
result, _ := json.Marshal(myChall)
fmt.Fprintln(w, string(result))
case "/session":
myToken.Success = true
myToken.Result.SessionToken = "foobar"
result, _ := json.Marshal(myToken)
fmt.Fprintln(w, string(result))
case "/session2":
myToken.Msg = "failed to get a session"
myToken.Success = false
result, _ := json.Marshal(myToken)
fmt.Fprintln(w, string(result))
default:
fmt.Fprintln(w, http.StatusNotFound)
}
}))
defer ts.Close()
ai := authInfo{}
ai.myAPI.login = ts.URL + "/login"
ai.myAPI.loginSession = ts.URL + "/session"
var mySessionToken string
st, err := getSessToken("token", &ai, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if st != "foobar" {
t.Error("Expected foobar, but got", st)
}
ai.myAPI.loginSession = ts.URL + "/session2"
_, err = getSessToken("token", &ai, &mySessionToken)
if err.Error() != "failed to get a session" {
t.Error("Expected but got failed to get a session, but got", err)
}
}

8
build.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -eu
ARCH=${1:-arm}
GOARCH=$ARCH go build -ldflags "-s -w"
ls -lh freebox_exporter
file freebox_exporter

59
changelog.md Normal file
View File

@ -0,0 +1,59 @@
# Changelog
## [1.3] - 2020-10-04
- Add VPN server metrics, mainly tx and rx for a user on a vpn with scr and local ip as labels
## [1.2.2] - 2020-10-03
- Change variable type int to int64 for RRD metrics, causing an error "constant overflow" on arm chipset
## [1.2] - 2020-05-27
- Add build script
- Log Freebox Server uptime and firmware version
- Log G.INP data
- Log connection status, protocol and modulation
- Log XDSL stats
- Don't log incorrect values (previously logged as zero)
- Remove dead code
## [1.1.9] - 2020-04-24
- Log freeplug speeds and connectivity
- Go 1.14
- Remove Godeps and vendored files
## [1.1.7] - 2019-09-04
- Go 1.13
## [1.1.6] - 2019-09-04
- There is no more uncomfortable error message when the application renews its token
- Adding a `-fiber` flag so that Freebox fiber users do not capture DSL metrics, which are empty on this type of Freebox
## [1.1.4] - 2019-09-03
- Adding a `-debug` flag to have more verbose error logs
## [1.1.2] - 2019-08-25
- Improve error messages
## [1.1.1] - 2019-07-31
- New Dockerfile for amd64 arch: reduce the image size to 3mb
## [1.1.0] - 2019-07-31
- Fix temp metrics
- Add Godeps
## [1.0.1] - 2019-07-30
- Change error catching
## [1.0.0] - 2019-07-29
- Rewriting the application by adding a ton of unit tests

9
contrib/README.md Normal file
View File

@ -0,0 +1,9 @@
Here you'll find related contributions to the project, such as ready-to-use Grafana dashboards (this will save you from having to reinvent the wheel when using the Freebox Exporter).
Thanks to [mcanevet](https://gist.github.com/mcanevet) for his contribution:
<img width="600" alt="mcanevet Grafana dashbord" src="https://user-images.githubusercontent.com/13923756/57589143-de912180-751f-11e9-8bad-7842b35776d9.png">
Thanks to [Pichon](https://github.com/lepichon) for his contribution:
<img width="600" alt="lepichon Grafana dashbord" src="https://user-images.githubusercontent.com/13923756/74086200-e9fa8480-4a80-11ea-92d4-a4aca6f8c159.png">

View File

@ -0,0 +1,544 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "6.6.0"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "stat",
"name": "Stat",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"cacheTimeout": null,
"datasource": "${DS_PROMETHEUS}",
"gridPos": {
"h": 3,
"w": 24,
"x": 0,
"y": 0
},
"id": 11,
"links": [],
"options": {
"colorMode": "value",
"fieldOptions": {
"calcs": [
"lastNotNull"
],
"defaults": {
"mappings": [],
"max": 1,
"min": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgb(33, 33, 33)",
"value": null
}
]
}
},
"overrides": [],
"values": false
},
"graphMode": "area",
"justifyMode": "auto",
"orientation": "vertical"
},
"pluginVersion": "6.6.0",
"repeat": null,
"targets": [
{
"expr": "freebox_lan_reachable == 1",
"instant": true,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Host on LAN",
"transparent": true,
"type": "stat"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 3
},
"hiddenSeries": false,
"id": 2,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"hideEmpty": false,
"max": true,
"min": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "/.*Trans.*/",
"transform": "negative-Y"
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "irate(freebox_net_down_bytes[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "Download Usage",
"refId": "A"
},
{
"expr": "freebox_net_bw_down_bytes / 10",
"legendFormat": "Download Bandwidth",
"refId": "B"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Network Traffic Download (bytes/sec)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": null,
"format": "Bps",
"label": "Bytes out (-) / in (+)",
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 11
},
"hiddenSeries": false,
"id": 13,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "/.*Trans.*/",
"transform": "negative-Y"
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "irate(freebox_net_up_bytes[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "Upload Usage",
"refId": "A"
},
{
"expr": "freebox_net_bw_up_bytes / 10",
"legendFormat": "Upload Bandwidth",
"refId": "B"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Network Traffic Upload (bytes/sec)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": null,
"format": "Bps",
"label": "Bytes out (-) / in (+)",
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"description": "",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 12,
"x": 0,
"y": 19
},
"hiddenSeries": false,
"id": 9,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pluginVersion": "6.6.0",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "freebox_system_fan_rpm",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Fan Speed",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": 3,
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "${DS_PROMETHEUS}",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 11,
"w": 12,
"x": 12,
"y": 19
},
"hiddenSeries": false,
"id": 6,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "freebox_system_temp_celsius",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "System Temperature",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "celsius",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": "5s",
"schemaVersion": 22,
"style": "dark",
"tags": [
"prometheus",
"freebox"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Freebox",
"uid": "TVsfEYmZk",
"version": 14
}

View File

@ -0,0 +1,418 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 5,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 2,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "/.*Trans.*/",
"transform": "negative-Y"
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "irate(freebox_net_down_bytes[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "Receive",
"refId": "B"
},
{
"expr": "irate(freebox_net_up_bytes[5m])",
"format": "time_series",
"intervalFactor": 2,
"legendFormat": "Transmit",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Network Traffic by bytes",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": null,
"format": "Bps",
"label": "Bytes out (-) / in (+)",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 4,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"rightSide": false,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "freebox_system_fan_rpm",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Fan RPM",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"decimals": null,
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"fill": 1,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 6,
"legend": {
"alignAsTable": true,
"avg": true,
"current": true,
"max": true,
"min": true,
"show": true,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "freebox_system_temp_celsius",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{name}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "System Temperature",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "celsius",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"#299c46",
"rgba(237, 129, 40, 0.89)",
"#d44a3a"
],
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 8,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"tableColumn": "",
"targets": [
{
"expr": "sum(freebox_lan_reachable)",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "",
"refId": "A"
}
],
"thresholds": "",
"timeFrom": null,
"timeShift": null,
"title": "Hosts on LAN",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
}
],
"refresh": "30s",
"schemaVersion": 18,
"style": "dark",
"tags": [
"prometheus",
"freebox"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Freebox",
"uid": "TVsfEYmZk",
"version": 14
}

255
gauges.go Normal file
View File

@ -0,0 +1,255 @@
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// XXX: see https://dev.freebox.fr/sdk/os/ for API documentation
// XXX: see https://prometheus.io/docs/practices/naming/ for metric names
// connectionXdsl
connectionXdslStatusUptimeGauges = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_status_uptime_seconds_total",
},
[]string{
"status",
"protocol",
"modulation",
},
)
connectionXdslDownAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_down_attn_decibels",
})
connectionXdslUpAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_up_attn_decibels",
})
connectionXdslDownSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_down_snr_decibels",
})
connectionXdslUpSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_up_snr_decibels",
})
connectionXdslErrorGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_errors_total",
Help: "Error counts",
},
[]string{
"direction", // up|down
"name", // crc|es|fec|hec
},
)
connectionXdslGinpGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_ginp",
},
[]string{
"direction", // up|down
"name", // enabled|rtx_(tx|c|uc)
},
)
connectionXdslNitroGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_connection_xdsl_nitro",
},
[]string{
"direction", // up|down
},
)
// RRD dsl [unstable]
rateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_dsl_up_bytes",
Help: "Available upload bandwidth (in byte/s)",
})
rateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_dsl_down_bytes",
Help: "Available download bandwidth (in byte/s)",
})
snrUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_dsl_snr_up_decibel",
Help: "Upload signal/noise ratio (in 1/10 dB)",
})
snrDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_dsl_snr_down_decibel",
Help: "Download signal/noise ratio (in 1/10 dB)",
})
// freeplug
freeplugRxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "freebox_freeplug_rx_rate_bits",
Help: "rx rate (from the freeplugs to the \"cco\" freeplug) (in bits/s) -1 if not available",
},
[]string{
"id",
},
)
freeplugTxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "freebox_freeplug_tx_rate_bits",
Help: "tx rate (from the \"cco\" freeplug to the freeplugs) (in bits/s) -1 if not available",
},
[]string{
"id",
},
)
freeplugHasNetworkGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "freebox_freeplug_has_network",
Help: "is connected to the network",
},
[]string{
"id",
},
)
// RRD Net [unstable]
bwUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_net_bw_up_bytes",
Help: "Upload available bandwidth (in byte/s)",
})
bwDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_net_bw_down_bytes",
Help: "Download available bandwidth (in byte/s)",
})
netRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_net_up_bytes",
Help: "Upload rate (in byte/s)",
})
netRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_net_down_bytes",
Help: "Download rate (in byte/s)",
})
vpnRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_net_vpn_up_bytes",
Help: "Vpn client upload rate (in byte/s)",
})
vpnRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
Name: "freebox_net_vpn_down_bytes",
Help: "Vpn client download rate (in byte/s)",
})
// Lan
lanReachableGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_lan_reachable",
Help: "Hosts reachable on LAN",
},
[]string{
"name", // hostname
"vendor",
"ip",
},
)
systemTempGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_system_temp_celsius",
Help: "Temperature sensors reported by system (in °C)",
},
[]string{
"name",
},
)
systemFanGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_system_fan_rpm",
Help: "Fan speed reported by system (in RPM)",
},
[]string{
"name",
},
)
systemUptimeGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_system_uptime_seconds_total",
},
[]string{
"firmware_version",
},
)
// wifi
wifiLabels = []string{
"access_point",
"hostname",
"state",
}
wifiSignalGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_signal_attenuation_db",
Help: "Wifi signal attenuation in decibel",
},
wifiLabels,
)
wifiInactiveGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_inactive_duration_seconds",
Help: "Wifi inactive duration in seconds",
},
wifiLabels,
)
wifiConnectionDurationGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_connection_duration_seconds",
Help: "Wifi connection duration in seconds",
},
wifiLabels,
)
wifiRXBytesGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_rx_bytes",
Help: "Wifi received data (from station to Freebox) in bytes",
},
wifiLabels,
)
wifiTXBytesGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_tx_bytes",
Help: "Wifi transmitted data (from Freebox to station) in bytes",
},
wifiLabels,
)
wifiRXRateGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_rx_rate",
Help: "Wifi reception data rate (from station to Freebox) in bytes/seconds",
},
wifiLabels,
)
wifiTXRateGauges = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "freebox_wifi_tx_rate",
Help: "Wifi transmission data rate (from Freebox to station) in bytes/seconds",
},
wifiLabels,
)
// vpn server connections list [unstable]
vpnServerConnectionsList = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "vpn_server_connections_list",
Help: "VPN server connections list",
},
[]string{
"user",
"vpn",
"src_ip",
"local_ip",
"name", // rx_bytes|tx_bytes
},
)
)

573
getters.go Normal file
View File

@ -0,0 +1,573 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
var (
apiErrors = map[string]error{
"invalid_token": errors.New("The app token you are trying to use is invalid or has been revoked"),
"insufficient_rights": errors.New("Your app permissions does not allow accessing this API"),
"denied_from_external_ip": errors.New("You are trying to get an app_token from a remote IP"),
"invalid_request": errors.New("Your request is invalid"),
"ratelimited": errors.New("Too many auth error have been made from your IP"),
"new_apps_denied": errors.New("New application token request has been disabled"),
"apps_denied": errors.New("API access from apps has been disabled"),
"internal_error": errors.New("Internal error"),
"db_error": errors.New("Oops, the database you are trying to access doesn't seem to exist"),
"nodev": errors.New("Invalid interface"),
}
)
func (r *rrd) status() error {
if apiErrors[r.ErrorCode] == nil {
return errors.New("RRD: The API returns an unknown error_code: " + r.ErrorCode)
}
return apiErrors[r.ErrorCode]
}
func (l *lan) status() error {
if apiErrors[l.ErrorCode] == nil {
return errors.New("LAN: The API returns an unknown error_code: " + l.ErrorCode)
}
return apiErrors[l.ErrorCode]
}
func setFreeboxToken(authInf *authInfo, xSessionToken *string) (string, error) {
token := os.Getenv("FREEBOX_TOKEN")
if token == "" {
var err error
*xSessionToken, err = getToken(authInf, xSessionToken)
if err != nil {
return "", err
}
token = *xSessionToken
}
if *xSessionToken == "" {
var err error
*xSessionToken, err = getSessToken(token, authInf, xSessionToken)
if err != nil {
log.Fatal(err)
}
token = *xSessionToken
}
return token, nil
}
func newPostRequest() *postRequest {
return &postRequest{
method: "POST",
url: mafreebox + "api/v4/rrd/",
header: "X-Fbx-App-Auth",
}
}
func getConnectionXdsl(authInf *authInfo, pr *postRequest, xSessionToken *string) (connectionXdsl, error) {
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return connectionXdsl{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return connectionXdsl{}, err
}
if resp.StatusCode == 404 {
return connectionXdsl{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return connectionXdsl{}, err
}
connectionXdslResp := connectionXdsl{}
err = json.Unmarshal(body, &connectionXdslResp)
if err != nil {
if debug {
log.Println(string(body))
}
return connectionXdsl{}, err
}
return connectionXdslResp, nil
}
func getDsl(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
d := &database{
DB: "dsl",
Fields: []string{"rate_up", "rate_down", "snr_up", "snr_down"},
Precision: 10,
DateStart: int(time.Now().Unix() - 10),
}
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
client := http.Client{}
r, err := json.Marshal(*d)
if err != nil {
return []int64{}, err
}
buf := bytes.NewReader(r)
req, err := http.NewRequest(pr.method, pr.url, buf)
if err != nil {
return []int64{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return []int64{}, err
}
if resp.StatusCode == 404 {
return []int64{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []int64{}, err
}
rrdTest := rrd{}
err = json.Unmarshal(body, &rrdTest)
if err != nil {
if debug {
log.Println(string(body))
}
return []int64{}, err
}
if rrdTest.ErrorCode == "auth_required" {
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
}
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
if rrdTest.status().Error() == "Unknown return code from the API" {
fmt.Println("getDsl")
}
return []int64{}, rrdTest.status()
}
if len(rrdTest.Result.Data) == 0 {
return []int64{}, nil
}
result := []int64{rrdTest.Result.Data[0]["rate_up"], rrdTest.Result.Data[0]["rate_down"], rrdTest.Result.Data[0]["snr_up"], rrdTest.Result.Data[0]["snr_down"]}
return result, nil
}
func getTemp(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
d := &database{
DB: "temp",
Fields: []string{"cpum", "cpub", "sw", "hdd", "fan_speed"},
Precision: 10,
DateStart: int(time.Now().Unix() - 10),
}
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
client := http.Client{}
r, err := json.Marshal(*d)
if err != nil {
return []int64{}, err
}
buf := bytes.NewReader(r)
req, err := http.NewRequest(pr.method, fmt.Sprintf(pr.url), buf)
if err != nil {
return []int64{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return []int64{}, err
}
if resp.StatusCode == 404 {
return []int64{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []int64{}, err
}
rrdTest := rrd{}
err = json.Unmarshal(body, &rrdTest)
if err != nil {
if debug {
log.Println(string(body))
}
return []int64{}, err
}
if rrdTest.ErrorCode == "auth_required" {
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
}
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
if rrdTest.status().Error() == "Unknown return code from the API" {
fmt.Println("getTemp")
}
return []int64{}, rrdTest.status()
}
if len(rrdTest.Result.Data) == 0 {
return []int64{}, nil
}
return []int64{rrdTest.Result.Data[0]["cpum"], rrdTest.Result.Data[0]["cpub"], rrdTest.Result.Data[0]["sw"], rrdTest.Result.Data[0]["hdd"], rrdTest.Result.Data[0]["fan_speed"]}, nil
}
func getNet(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
d := &database{
DB: "net",
Fields: []string{"bw_up", "bw_down", "rate_up", "rate_down", "vpn_rate_up", "vpn_rate_down"},
Precision: 10,
DateStart: int(time.Now().Unix() - 10),
}
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
client := http.Client{}
r, err := json.Marshal(*d)
if err != nil {
return []int64{}, err
}
buf := bytes.NewReader(r)
req, err := http.NewRequest(pr.method, pr.url, buf)
if err != nil {
return []int64{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return []int64{}, err
}
if resp.StatusCode == 404 {
return []int64{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []int64{}, err
}
rrdTest := rrd{}
err = json.Unmarshal(body, &rrdTest)
if err != nil {
if debug {
log.Println(string(body))
}
return []int64{}, err
}
if rrdTest.ErrorCode == "auth_required" {
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
}
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
if rrdTest.status().Error() == "Unknown return code from the API" {
fmt.Println("getNet")
}
return []int64{}, rrdTest.status()
}
if len(rrdTest.Result.Data) == 0 {
return []int64{}, nil
}
return []int64{rrdTest.Result.Data[0]["bw_up"], rrdTest.Result.Data[0]["bw_down"], rrdTest.Result.Data[0]["rate_up"], rrdTest.Result.Data[0]["rate_down"], rrdTest.Result.Data[0]["vpn_rate_up"], rrdTest.Result.Data[0]["vpn_rate_down"]}, nil
}
func getSwitch(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
d := &database{
DB: "switch",
Fields: []string{"rx_1", "tx_1", "rx_2", "tx_2", "rx_3", "tx_3", "rx_4", "tx_4"},
Precision: 10,
DateStart: int(time.Now().Unix() - 10),
}
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
client := http.Client{}
r, err := json.Marshal(*d)
if err != nil {
return []int64{}, err
}
buf := bytes.NewReader(r)
req, err := http.NewRequest(pr.method, pr.url, buf)
if err != nil {
return []int64{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return []int64{}, err
}
if resp.StatusCode == 404 {
return []int64{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []int64{}, err
}
rrdTest := rrd{}
err = json.Unmarshal(body, &rrdTest)
if err != nil {
if debug {
log.Println(string(body))
}
return []int64{}, err
}
if rrdTest.ErrorCode == "auth_required" {
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
if err != nil {
return []int64{}, err
}
}
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
if rrdTest.status().Error() == "Unknown return code from the API" {
fmt.Println("getSwitch")
}
return []int64{}, rrdTest.status()
}
if len(rrdTest.Result.Data) == 0 {
return []int64{}, nil
}
return []int64{rrdTest.Result.Data[0]["rx_1"], rrdTest.Result.Data[0]["tx_1"], rrdTest.Result.Data[0]["rx_2"], rrdTest.Result.Data[0]["tx_2"], rrdTest.Result.Data[0]["rx_3"], rrdTest.Result.Data[0]["tx_3"], rrdTest.Result.Data[0]["rx_4"], rrdTest.Result.Data[0]["tx_4"]}, nil
}
func getLan(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]lanHost, error) {
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
if err != nil {
return []lanHost{}, err
}
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return []lanHost{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return []lanHost{}, err
}
if resp.StatusCode == 404 {
return []lanHost{}, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []lanHost{}, err
}
lanResp := lan{}
err = json.Unmarshal(body, &lanResp)
if err != nil {
if debug {
log.Println(string(body))
}
return []lanHost{}, err
}
if lanResp.ErrorCode == "auth_required" {
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
if err != nil {
return []lanHost{}, err
}
}
if lanResp.ErrorCode != "" && lanResp.ErrorCode != "auth_required" {
return []lanHost{}, lanResp.status()
}
return lanResp.Result, nil
}
func getFreeplug(authInf *authInfo, pr *postRequest, xSessionToken *string) (freeplug, error) {
if _, err := setFreeboxToken(authInf, xSessionToken); err != nil {
return freeplug{}, err
}
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return freeplug{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return freeplug{}, err
}
if resp.StatusCode == 404 {
return freeplug{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return freeplug{}, err
}
freeplugResp := freeplug{}
err = json.Unmarshal(body, &freeplugResp)
if err != nil {
if debug {
log.Println(string(body))
}
return freeplug{}, err
}
return freeplugResp, nil
}
func getSystem(authInf *authInfo, pr *postRequest, xSessionToken *string) (system, error) {
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return system{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return system{}, err
}
if resp.StatusCode == 404 {
return system{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return system{}, err
}
systemResp := system{}
err = json.Unmarshal(body, &systemResp)
if err != nil {
if debug {
log.Println(string(body))
}
return system{}, err
}
return systemResp, nil
}
func getWifi(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifi, error) {
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return wifi{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return wifi{}, err
}
if resp.StatusCode == 404 {
return wifi{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return wifi{}, err
}
wifiResp := wifi{}
err = json.Unmarshal(body, &wifiResp)
if err != nil {
if debug {
log.Println(string(body))
}
return wifi{}, err
}
return wifiResp, nil
}
func getWifiStations(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifiStations, error) {
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return wifiStations{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return wifiStations{}, err
}
if resp.StatusCode == 404 {
return wifiStations{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return wifiStations{}, err
}
wifiStationResp := wifiStations{}
err = json.Unmarshal(body, &wifiStationResp)
if err != nil {
if debug {
log.Println(string(body))
}
return wifiStations{}, err
}
return wifiStationResp, nil
}
func getVpnServer(authInf *authInfo, pr *postRequest, xSessionToken *string) (vpnServer, error) {
client := http.Client{}
req, err := http.NewRequest(pr.method, pr.url, nil)
if err != nil {
return vpnServer{}, err
}
req.Header.Add(pr.header, *xSessionToken)
resp, err := client.Do(req)
if err != nil {
return vpnServer{}, err
}
if resp.StatusCode == 404 {
return vpnServer{}, errors.New(resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return vpnServer{}, err
}
vpnServerResp := vpnServer{}
err = json.Unmarshal(body, &vpnServerResp)
if err != nil {
if debug {
log.Println(string(body))
}
return vpnServer{}, err
}
return vpnServerResp, nil
}

727
getters_test.go Normal file
View File

@ -0,0 +1,727 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
)
func TestSetFreeboxToken(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/login":
myChall := &challenge{
Success: true,
}
myChall.Result.Challenge = "foobar"
result, _ := json.Marshal(myChall)
fmt.Fprintln(w, string(result))
case "/session":
myToken := sessionToken{
Success: true,
}
myToken.Result.SessionToken = "foobar"
result, _ := json.Marshal(myToken)
fmt.Fprintln(w, string(result))
case "/granted/":
myTrack := track{
Success: true,
}
myTrack.Result.TrackID = 101
result, _ := json.Marshal(myTrack)
fmt.Fprintln(w, string(result))
case "/granted/101":
myGrant := grant{
Success: true,
}
myGrant.Result.Status = "granted"
result, _ := json.Marshal(myGrant)
fmt.Fprintln(w, string(result))
default:
fmt.Fprintln(w, http.StatusNotFound)
}
}))
defer ts.Close()
ai := &authInfo{}
ai.myStore.location = "/tmp/token"
ai.myAPI.login = ts.URL + "/login"
ai.myAPI.loginSession = ts.URL + "/session"
ai.myAPI.authz = ts.URL + "/granted/"
ai.myReader = bufio.NewReader(strings.NewReader("\n"))
var mySessionToken string
token, err := setFreeboxToken(ai, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
defer os.Remove(ai.myStore.location)
if token != "foobar" {
t.Error("Expected foobar, but got", token)
}
os.Setenv("FREEBOX_TOKEN", "barfoo")
defer os.Unsetenv("FREEBOX_TOKEN")
token, err = setFreeboxToken(ai, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if token != "barfoo" {
t.Error("Expected barfoo, but got", token)
}
}
func TestGetDsl(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/good":
myRRD := rrd{
Success: true,
}
myRRD.Result.Data = []map[string]int64{
{
"rate_up": 12,
"rate_down": 34,
"snr_up": 56,
"snr_down": 78,
},
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/error":
myRRD := rrd{
Success: true,
ErrorCode: "insufficient_rights",
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/null":
myRRD := rrd{
Success: true,
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
}
}))
defer ts.Close()
goodPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/good",
}
errorPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/error",
}
nullPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/null",
}
ai := &authInfo{}
mySessionToken := "foobar"
getDslResult, err := getDsl(ai, goodPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if getDslResult[0] != 12 || getDslResult[1] != 34 || getDslResult[2] != 56 || getDslResult[3] != 78 {
t.Errorf("Expected 12 34 56 78, but got %v %v %v %v\n", getDslResult[0], getDslResult[1], getDslResult[2], getDslResult[3])
}
getDslResult, err = getDsl(ai, errorPR, &mySessionToken)
if err.Error() != "Your app permissions does not allow accessing this API" {
t.Error("Expected Your app permissions does not allow accessing this API, but go", err)
}
if len(getDslResult) != 0 {
t.Error("Expected 0, but got", len(getDslResult))
}
getDslResult, err = getDsl(ai, nullPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if len(getDslResult) != 0 {
t.Error("Expected 0, but got", len(getDslResult))
}
}
func TestGetTemp(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/good":
myRRD := rrd{
Success: true,
}
myRRD.Result.Data = []map[string]int64{
{
"cpum": 01,
"cpub": 02,
"sw": 03,
"hdd": 04,
"fan_speed": 05,
},
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/error":
myRRD := rrd{
Success: true,
ErrorCode: "denied_from_external_ip",
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/null":
myRRD := rrd{
Success: true,
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
}
}))
defer ts.Close()
goodPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/good",
}
errorPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/error",
}
nullPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/null",
}
ai := &authInfo{}
mySessionToken := "foobar"
getTempResult, err := getTemp(ai, goodPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if getTempResult[0] != 01 || getTempResult[1] != 02 || getTempResult[2] != 03 || getTempResult[3] != 04 || getTempResult[4] != 05 {
t.Errorf("Expected 01 02 03 04 05, but got %v %v %v %v %v\n", getTempResult[0], getTempResult[1], getTempResult[2], getTempResult[3], getTempResult[4])
}
getTempResult, err = getTemp(ai, errorPR, &mySessionToken)
if err.Error() != "You are trying to get an app_token from a remote IP" {
t.Error("Expected You are trying to get an app_token from a remote IP, but go", err)
}
if len(getTempResult) != 0 {
t.Error("Expected 0, but got", len(getTempResult))
}
getTempResult, err = getTemp(ai, nullPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if len(getTempResult) != 0 {
t.Error("Expected 0, but got", len(getTempResult))
}
}
func TestGetNet(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/good":
myRRD := rrd{
Success: true,
}
myRRD.Result.Data = []map[string]int64{
{
"bw_up": 12500000000,
"bw_down": 12500000000,
"rate_up": 12500000000,
"rate_down": 12500000000,
"vpn_rate_up": 12500000000,
"vpn_rate_down": 12500000000,
},
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/error":
myRRD := rrd{
Success: true,
ErrorCode: "new_apps_denied",
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/null":
myRRD := rrd{
Success: true,
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
}
}))
defer ts.Close()
goodPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/good",
}
errorPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/error",
}
nullPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/null",
}
ai := &authInfo{}
mySessionToken := "foobar"
getNetResult, err := getNet(ai, goodPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but go", err)
}
if getNetResult[0] != 12500000000 || getNetResult[1] != 12500000000 || getNetResult[2] != 12500000000 || getNetResult[3] != 12500000000 || getNetResult[4] != 12500000000 || getNetResult[5] != 12500000000 {
t.Errorf("Expected 01 02 03 04 05 06, but got %v %v %v %v %v %v\n", getNetResult[0], getNetResult[1], getNetResult[2], getNetResult[3], getNetResult[4], getNetResult[5])
}
getNetResult, err = getNet(ai, errorPR, &mySessionToken)
if err.Error() != "New application token request has been disabled" {
t.Error("Expected New application token request has been disabled, but got", err)
}
if len(getNetResult) != 0 {
t.Error("Expected 0, but got", len(getNetResult))
}
getNetResult, err = getNet(ai, nullPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if len(getNetResult) != 0 {
t.Error("Expected 0, but got", len(getNetResult))
}
}
func TestGetSwitch(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/good":
myRRD := rrd{
Success: true,
}
myRRD.Result.Data = []map[string]int64{
{
"rx_1": 01,
"tx_1": 11,
"rx_2": 02,
"tx_2": 12,
"rx_3": 03,
"tx_3": 13,
"rx_4": 04,
"tx_4": 14,
},
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/error":
myRRD := rrd{
Success: true,
ErrorCode: "apps_denied",
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
case "/null":
myRRD := rrd{
Success: true,
}
result, _ := json.Marshal(myRRD)
fmt.Fprintln(w, string(result))
}
}))
defer ts.Close()
goodPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/good",
}
errorPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/error",
}
nullPR := &postRequest{
method: "POST",
header: "X-Fbx-App-Auth",
url: ts.URL + "/null",
}
ai := &authInfo{}
mySessionToken := "foobar"
getSwitchResult, err := getSwitch(ai, goodPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if getSwitchResult[0] != 01 || getSwitchResult[1] != 11 || getSwitchResult[2] != 02 || getSwitchResult[3] != 12 || getSwitchResult[4] != 03 || getSwitchResult[5] != 13 || getSwitchResult[6] != 04 || getSwitchResult[7] != 14 {
t.Errorf("Expected 01 11 02 12 03 13 04 14, but got %v %v %v %v %v %v %v %v\n", getSwitchResult[0], getSwitchResult[1], getSwitchResult[2], getSwitchResult[3], getSwitchResult[4], getSwitchResult[5], getSwitchResult[6], getSwitchResult[7])
}
getSwitchResult, err = getSwitch(ai, errorPR, &mySessionToken)
if err.Error() != "API access from apps has been disabled" {
t.Error("Expected API access from apps has been disabled, but got", err)
}
if len(getSwitchResult) != 0 {
t.Error("Expected 0, but got", len(getSwitchResult))
}
getSwitchResult, err = getSwitch(ai, nullPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if len(getSwitchResult) != 0 {
t.Error("Expected 0, but got", len(getSwitchResult))
}
}
func TestGetLan(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/good":
myLan := lan{
Success: true,
}
myLan.Result = []lanHost{
{
Reachable: true,
PrimaryName: "Reachable host",
},
{
Reachable: false,
PrimaryName: "Unreachable host",
},
}
result, _ := json.Marshal(myLan)
fmt.Fprintln(w, string(result))
case "/error":
myLan := lan{
Success: true,
ErrorCode: "ratelimited",
}
result, _ := json.Marshal(myLan)
fmt.Fprintln(w, string(result))
}
}))
defer ts.Close()
goodPR := &postRequest{
method: "GET",
header: "X-Fbx-App-Auth",
url: ts.URL + "/good",
}
errorPR := &postRequest{
method: "GET",
header: "X-Fbx-App-Auth",
url: ts.URL + "/error",
}
ai := &authInfo{}
mySessionToken := "foobar"
lanAvailable, err := getLan(ai, goodPR, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
for _, v := range lanAvailable {
if v.Reachable && v.PrimaryName != "Reachable host" {
t.Errorf("Expected Reachable: true, Host: Reachable host, but go Reachable: %v, Host: %v", v.Reachable, v.PrimaryName)
}
if !v.Reachable && v.PrimaryName != "Unreachable host" {
t.Errorf("Expected Reachable: false, Host: Unreachable host, but go Reachable: %v, Host: %v", !v.Reachable, v.PrimaryName)
}
}
lanAvailable, err = getLan(ai, errorPR, &mySessionToken)
if err.Error() != "Too many auth error have been made from your IP" {
t.Error("Expected Too many auth error have been made from your IP, but got", err)
}
}
func TestGetSystem(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mySys := system{
Success: true,
}
mySys.Result.FanRPM = 666
mySys.Result.TempCpub = 81
mySys.Result.TempCpum = 89
mySys.Result.TempHDD = 30
mySys.Result.TempSW = 54
/*
mySys.Result {
FanRPM: 666,
TempCpub: 81,
TempCpum: 89,
TempHDD: 30,
TempSW: 54,
}
*/
result, _ := json.Marshal(mySys)
fmt.Fprintln(w, string(result))
}))
defer ts.Close()
pr := &postRequest{
method: "GET",
header: "X-Fbx-App-Auth",
url: ts.URL,
}
ai := &authInfo{}
mySessionToken := "foobar"
systemStats, err := getSystem(ai, pr, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if systemStats.Result.FanRPM != 666 {
t.Error("Expected 666, but got", systemStats.Result.FanRPM)
}
if systemStats.Result.TempCpub != 81 {
t.Error("Expected 81, but got", systemStats.Result.TempCpub)
}
if systemStats.Result.TempCpum != 89 {
t.Error("Expected 89, but got", systemStats.Result.TempCpum)
}
if systemStats.Result.TempHDD != 30 {
t.Error("Expected 30, but got", systemStats.Result.TempHDD)
}
if systemStats.Result.TempSW != 54 {
t.Error("Expected 54, but got", systemStats.Result.TempSW)
}
}
func TestGetWifi(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
myWifi := wifi{
Success: true,
}
myAP := wifiAccessPoint{
Name: "AP1",
ID: 0,
}
myWifi.Result = []wifiAccessPoint{myAP}
result, _ := json.Marshal(myWifi)
fmt.Fprintln(w, string(result))
}))
defer ts.Close()
pr := &postRequest{
method: "GET",
header: "X-Fbx-App-Auth",
url: ts.URL,
}
ai := &authInfo{}
mySessionToken := "foobar"
wifiStats, err := getWifi(ai, pr, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if wifiStats.Result[0].Name != "AP1" {
t.Error("Expected AP1, but got", wifiStats.Result[0].Name)
}
if wifiStats.Result[0].ID != 0 {
t.Error("Expected 0, but got", wifiStats.Result[0].ID)
}
}
func TestGetWifiStations(t *testing.T) {
os.Setenv("FREEBOX_TOKEN", "IOI")
defer os.Unsetenv("FREEBOX_TOKEN")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
myWifiStations := wifiStations{
Success: true,
}
myStation := wifiStation{
Hostname: "station_host",
MAC: "AA:BB:CC:DD:EE:FF",
State: "authorized",
Inactive: 60,
RXBytes: 500,
TXBytes: 2280000000,
ConnectionDuration: 600,
TXRate: 4260000000,
RXRate: 5,
Signal: -20,
}
myWifiStations.Result = []wifiStation{myStation}
result, _ := json.Marshal(myWifiStations)
fmt.Fprintln(w, string(result))
}))
defer ts.Close()
pr := &postRequest{
method: "GET",
header: "X-Fbx-App-Auth",
url: ts.URL,
}
ai := &authInfo{}
mySessionToken := "foobar"
wifiStationsStats, err := getWifiStations(ai, pr, &mySessionToken)
if err != nil {
t.Error("Expected no err, but got", err)
}
if wifiStationsStats.Result[0].Hostname != "station_host" {
t.Error("Expected station_host, but got", wifiStationsStats.Result[0].Hostname)
}
if wifiStationsStats.Result[0].MAC != "AA:BB:CC:DD:EE:FF" {
t.Error("Expected AA:BB:CC:DD:EE:FF, but got", wifiStationsStats.Result[0].MAC)
}
if wifiStationsStats.Result[0].State != "authorized" {
t.Error("Expected authorized, but got", wifiStationsStats.Result[0].State)
}
if wifiStationsStats.Result[0].Inactive != 60 {
t.Error("Expected 60, but got", wifiStationsStats.Result[0].Inactive)
}
if wifiStationsStats.Result[0].RXBytes != 500 {
t.Error("Expected 500, but got", wifiStationsStats.Result[0].RXBytes)
}
if wifiStationsStats.Result[0].TXBytes != 10000 {
t.Error("Expected 10000, but got", wifiStationsStats.Result[0].TXBytes)
}
if wifiStationsStats.Result[0].ConnectionDuration != 600 {
t.Error("Expected 600, but got", wifiStationsStats.Result[0].ConnectionDuration)
}
if wifiStationsStats.Result[0].TXRate != 20 {
t.Error("Expected 20, but got", wifiStationsStats.Result[0].TXRate)
}
if wifiStationsStats.Result[0].RXRate != 5 {
t.Error("Expected 5, but got", wifiStationsStats.Result[0].RXRate)
}
if wifiStationsStats.Result[0].Signal != -20 {
t.Error("Expected -20, but got", wifiStationsStats.Result[0].Signal)
}
}
func Test_getNet(t *testing.T) {
type args struct {
authInf *authInfo
pr *postRequest
xSessionToken *string
}
tests := []struct {
name string
args args
want []int
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getNet(tt.args.authInf, tt.args.pr, tt.args.xSessionToken)
if (err != nil) != tt.wantErr {
t.Errorf("getNet() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getNet() = %v, want %v", got, tt.want)
}
})
}
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module freebox_exporter
go 1.14
require (
github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c // indirect
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
github.com/prometheus/client_golang v0.9.2
)

23
go.sum Normal file
View File

@ -0,0 +1,23 @@
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c h1:fQ4P1oAipLwec/j5tfZTYV/e5i9ICSk23uVL+TK9III=
github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

310
main.go Normal file
View File

@ -0,0 +1,310 @@
package main
import (
"bufio"
"flag"
"log"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/iancoleman/strcase"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
mafreebox string
listen string
debug bool
fiber bool
)
func init() {
flag.StringVar(&mafreebox, "endpoint", "http://mafreebox.freebox.fr/", "Endpoint for freebox API")
flag.StringVar(&listen, "listen", ":10001", "Prometheus metrics port")
flag.BoolVar(&debug, "debug", false, "Debug mode")
flag.BoolVar(&fiber, "fiber", false, "Turn on if you're using a fiber Freebox")
}
func main() {
flag.Parse()
if !strings.HasSuffix(mafreebox, "/") {
mafreebox = mafreebox + "/"
}
endpoint := mafreebox + "api/v4/login/"
myAuthInfo := &authInfo{
myAPI: api{
login: endpoint,
authz: endpoint + "authorize/",
loginSession: endpoint + "session/",
},
myStore: store{location: os.Getenv("HOME") + "/.freebox_token"},
myApp: app{
AppID: "fr.freebox.exporter",
AppName: "prometheus-exporter",
AppVersion: "0.4",
DeviceName: "local",
},
myReader: bufio.NewReader(os.Stdin),
}
myPostRequest := newPostRequest()
myConnectionXdslRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v4/connection/xdsl/",
header: "X-Fbx-App-Auth",
}
myFreeplugRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v4/freeplug/",
header: "X-Fbx-App-Auth",
}
myLanRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v4/lan/browser/pub/",
header: "X-Fbx-App-Auth",
}
mySystemRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v4/system/",
header: "X-Fbx-App-Auth",
}
myWifiRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v2/wifi/ap/",
header: "X-Fbx-App-Auth",
}
myVpnRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v4/vpn/connection/",
header: "X-Fbx-App-Auth",
}
var mySessionToken string
go func() {
for {
// There is no DSL metric on fiber Freebox
// If you use a fiber Freebox, use -fiber flag to turn off this metric
if !fiber {
// connectionXdsl metrics
connectionXdslStats, err := getConnectionXdsl(myAuthInfo, myConnectionXdslRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with connectionXdsl metrics: %v", err)
}
if connectionXdslStats.Success {
status := connectionXdslStats.Result.Status
result := connectionXdslStats.Result
down := result.Down
up := result.Up
connectionXdslStatusUptimeGauges.
WithLabelValues(status.Status, status.Protocol, status.Modulation).
Set(float64(status.Uptime))
connectionXdslDownAttnGauge.Set(float64(down.Attn10) / 10)
connectionXdslUpAttnGauge.Set(float64(up.Attn10) / 10)
// XXX: sometimes the Freebox is reporting zero as SNR which
// does not make sense so we don't log these
if down.Snr10 > 0 {
connectionXdslDownSnrGauge.Set(float64(down.Snr10) / 10)
}
if up.Snr10 > 0 {
connectionXdslUpSnrGauge.Set(float64(up.Snr10) / 10)
}
connectionXdslNitroGauges.WithLabelValues("down").
Set(bool2float(down.Nitro))
connectionXdslNitroGauges.WithLabelValues("up").
Set(bool2float(up.Nitro))
connectionXdslGinpGauges.WithLabelValues("down", "enabled").
Set(bool2float(down.Ginp))
connectionXdslGinpGauges.WithLabelValues("up", "enabled").
Set(bool2float(up.Ginp))
logFields(result, connectionXdslGinpGauges,
[]string{"rtx_tx", "rtx_c", "rtx_uc"})
logFields(result, connectionXdslErrorGauges,
[]string{"crc", "es", "fec", "hec", "ses"})
}
// dsl metrics
getDslResult, err := getDsl(myAuthInfo, myPostRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with DSL metrics: %v", err)
}
if len(getDslResult) > 0 {
rateUpGauge.Set(float64(getDslResult[0]))
rateDownGauge.Set(float64(getDslResult[1]))
snrUpGauge.Set(float64(getDslResult[2]))
snrDownGauge.Set(float64(getDslResult[3]))
}
}
// freeplug metrics
freeplugStats, err := getFreeplug(myAuthInfo, myFreeplugRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with freeplug metrics: %v", err)
}
for _, freeplugNetwork := range freeplugStats.Result {
for _, freeplugMember := range freeplugNetwork.Members {
if freeplugMember.HasNetwork {
freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(1))
} else {
freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(0))
}
Mb := 1e6
rxRate := float64(freeplugMember.RxRate) * Mb
txRate := float64(freeplugMember.TxRate) * Mb
if rxRate >= 0 { // -1 if not unavailable
freeplugRxRateGauge.WithLabelValues(freeplugMember.ID).Set(rxRate)
}
if txRate >= 0 { // -1 if not unavailable
freeplugTxRateGauge.WithLabelValues(freeplugMember.ID).Set(txRate)
}
}
}
// net metrics
getNetResult, err := getNet(myAuthInfo, myPostRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with NET metrics: %v", err)
}
if len(getNetResult) > 0 {
bwUpGauge.Set(float64(getNetResult[0]))
bwDownGauge.Set(float64(getNetResult[1]))
netRateUpGauge.Set(float64(getNetResult[2]))
netRateDownGauge.Set(float64(getNetResult[3]))
vpnRateUpGauge.Set(float64(getNetResult[4]))
vpnRateDownGauge.Set(float64(getNetResult[5]))
}
// lan metrics
lanAvailable, err := getLan(myAuthInfo, myLanRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with LAN metrics: %v", err)
}
for _, v := range lanAvailable {
var Ip string
if len(v.L3c) > 0 {
Ip = v.L3c[0].Addr
} else {
Ip = ""
}
if v.Reachable {
lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(1))
} else {
lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(0))
}
}
// system metrics
systemStats, err := getSystem(myAuthInfo, mySystemRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with System metrics: %v", err)
}
systemTempGauges.WithLabelValues("Température CPU B").Set(float64(systemStats.Result.TempCpub))
systemTempGauges.WithLabelValues("Température CPU M").Set(float64(systemStats.Result.TempCpum))
systemTempGauges.WithLabelValues("Température Switch").Set(float64(systemStats.Result.TempSW))
systemTempGauges.WithLabelValues("Disque dur").Set(float64(systemStats.Result.TempHDD))
systemFanGauges.WithLabelValues("Ventilateur 1").Set(float64(systemStats.Result.FanRPM))
systemUptimeGauges.
WithLabelValues(systemStats.Result.FirmwareVersion).
Set(float64(systemStats.Result.UptimeVal))
// wifi metrics
wifiStats, err := getWifi(myAuthInfo, myWifiRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with Wifi metrics: %v", err)
}
for _, accessPoint := range wifiStats.Result {
myWifiStationRequest := &postRequest{
method: "GET",
url: mafreebox + "api/v2/wifi/ap/" + strconv.Itoa(accessPoint.ID) + "/stations",
header: "X-Fbx-App-Auth",
}
wifiStationsStats, err := getWifiStations(myAuthInfo, myWifiStationRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with Wifi station metrics: %v", err)
}
for _, station := range wifiStationsStats.Result {
wifiSignalGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Signal))
wifiInactiveGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Inactive))
wifiConnectionDurationGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.ConnectionDuration))
wifiRXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXBytes))
wifiTXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXBytes))
wifiRXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXRate))
wifiTXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXRate))
}
}
// VPN Server Connections List
getVpnServerResult, err := getVpnServer(myAuthInfo, myVpnRequest, &mySessionToken)
if err != nil {
log.Printf("An error occured with VPN station metrics: %v", err)
}
for _, connection := range getVpnServerResult.Result {
vpnServerConnectionsList.With(prometheus.Labels{"user": connection.User, "vpn": connection.Vpn, "src_ip": connection.SrcIP, "local_ip": connection.LocalIP, "name": "rx_bytes"}).Set(float64(connection.RxBytes))
vpnServerConnectionsList.With(prometheus.Labels{"user": connection.User, "vpn": connection.Vpn, "src_ip": connection.SrcIP, "local_ip": connection.LocalIP, "name": "tx_bytes"}).Set(float64(connection.TxBytes))
}
time.Sleep(10 * time.Second)
}
}()
log.Println("freebox_exporter started on port", listen)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(listen, nil))
}
func logFields(result interface{}, gauge *prometheus.GaugeVec, fields []string) error {
resultReflect := reflect.ValueOf(result)
for _, direction := range []string{"down", "up"} {
for _, field := range fields {
value := reflect.Indirect(resultReflect).
FieldByName(strcase.ToCamel(direction)).
FieldByName(strcase.ToCamel(field))
if value.IsZero() {
continue
}
gauge.WithLabelValues(direction, field).
Set(float64(value.Int()))
}
}
return nil
}
func bool2float(b bool) float64 {
if b {
return 1
}
return 0
}

276
structs.go Normal file
View File

@ -0,0 +1,276 @@
package main
import "bufio"
type track struct {
Success bool `json:"success"`
Result struct {
AppToken string `json:"app_token"`
TrackID int `json:"track_id"`
} `json:"result"`
}
type grant struct {
Success bool `json:"success"`
Result struct {
Status string `json:"status"`
Challenge string `json:"challenge"`
} `json:"result"`
}
type challenge struct {
Success bool `json:"success"`
Result struct {
LoggedIN bool `json:"logged_in,omitempty"`
Challenge string `json:"challenge"`
} `json:"result"`
}
type session struct {
AppID string `json:"app_id"`
Password string `json:"password"`
}
type sessionToken struct {
Msg string `json:"msg,omitempty"`
Success bool `json:"success"`
UID string `json:"uid,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
Result struct {
SessionToken string `json:"session_token,omitempty"`
Challenge string `json:"challenge"`
Permissions struct {
Settings bool `json:"settings,omitempty"`
Contacts bool `json:"contacts,omitempty"`
Calls bool `json:"calls,omitempty"`
Explorer bool `json:"explorer,omitempty"`
Downloader bool `json:"downloader,omitempty"`
Parental bool `json:"parental,omitempty"`
Pvr bool `json:"pvr,omitempty"`
Home bool `json:"home,omitempty"`
Camera bool `json:"camera,omitempty"`
} `json:"permissions,omitempty"`
} `json:"result"`
}
type rrd struct {
UID string `json:"uid,omitempty"`
Success bool `json:"success"`
Msg string `json:"msg,omitempty"`
Result struct {
DateStart int `json:"date_start,omitempty"`
DateEnd int `json:"date_end,omitempty"`
Data []map[string]int64 `json:"data,omitempty"`
} `json:"result"`
ErrorCode string `json:"error_code"`
}
// https://dev.freebox.fr/sdk/os/connection/
type connectionXdsl struct {
Success bool `json:"success"`
Result struct {
Status struct {
Status string `json:"status"`
Modulation string `json:"modulation"`
Protocol string `json:"protocol"`
Uptime int `json:"uptime"`
} `json:"status"`
Down struct {
Attn int `json:"attn"`
Attn10 int `json:"attn_10"`
Crc int `json:"crc"`
Es int `json:"es"`
Fec int `json:"fec"`
Ginp bool `json:"ginp"`
Hec int `json:"hec"`
Maxrate uint64 `json:"maxrate"`
Nitro bool `json:"nitro"`
Phyr bool `json:"phyr"`
Rate int `json:"rate"`
RtxC int `json:"rtx_c,omitempty"`
RtxTx int `json:"rtx_tx,omitempty"`
RtxUc int `json:"rtx_uc,omitempty"`
Rxmt int `json:"rxmt"`
RxmtCorr int `json:"rxmt_corr"`
RxmtUncorr int `json:"rxmt_uncorr"`
Ses int `json:"ses"`
Snr int `json:"snr"`
Snr10 int `json:"snr_10"`
} `json:"down"`
Up struct {
Attn int `json:"attn"`
Attn10 int `json:"attn_10"`
Crc int `json:"crc"`
Es int `json:"es"`
Fec int `json:"fec"`
Ginp bool `json:"ginp"`
Hec int `json:"hec"`
Maxrate uint64 `json:"maxrate"`
Nitro bool `json:"nitro"`
Phyr bool `json:"phyr"`
Rate uint64 `json:"rate"`
RtxC int `json:"rtx_c,omitempty"`
RtxTx int `json:"rtx_tx,omitempty"`
RtxUc int `json:"rtx_uc,omitempty"`
Rxmt int `json:"rxmt"`
RxmtCorr int `json:"rxmt_corr"`
RxmtUncorr int `json:"rxmt_uncorr"`
Ses int `json:"ses"`
Snr int `json:"snr"`
Snr10 int `json:"snr_10"`
} `json:"up"`
}
}
type database struct {
DB string `json:"db"`
DateStart int `json:"date_start,omitempty"`
DateEnd int `json:"date_end,omitempty"`
Precision int `json:"precision,omitempty"`
Fields []string `json:"fields"`
}
// https://dev.freebox.fr/sdk/os/freeplug/
type freeplug struct {
Success bool `json:"success"`
Result []freeplugNetwork `json:"result"`
}
type freeplugNetwork struct {
ID string `json:"id"`
Members []freeplugMember `json:"members"`
}
type freeplugMember struct {
ID string `json:"id"`
Local bool `json:"local"`
NetRole string `json:"net_role"`
EthPortStatus string `json:"eth_port_status"`
EthFullDuplex bool `json:"eth_full_duplex"`
HasNetwork bool `json:"has_network"`
EthSpeed int `json:"eth_speed"`
Inative int `json:"inactive"`
NetID string `json:"net_id"`
RxRate int64 `json:"rx_rate"`
TxRate int64 `json:"tx_rate"`
Model string `json:"model"`
}
// https://dev.freebox.fr/sdk/os/lan/
type l3c struct {
Addr string `json:"addr,omitempty"`
}
type lanHost struct {
Reachable bool `json:"reachable,omitempty"`
PrimaryName string `json:"primary_name,omitempty"`
Vendor_name string `json:"vendor_name,omitempty"`
L3c []l3c `json:"l3connectivities,omitempty"`
}
type lan struct {
Success bool `json:"success"`
Result []lanHost `json:"result"`
ErrorCode string `json:"error_code"`
}
type idNameValue struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Value int `json:"value,omitempty"`
}
// https://dev.freebox.fr/sdk/os/system/
type system struct {
Success bool `json:"success"`
Result struct {
Mac string `json:"mac,omitempty"`
FanRPM int `json:"fan_rpm,omitempty"`
BoxFlavor string `json:"box_flavor,omitempty"`
TempCpub int `json:"temp_cpub,omitempty"`
TempCpum int `json:"temp_cpum,omitempty"`
DiskStatus string `json:"disk_status,omitempty"`
TempHDD int `json:"temp_hdd,omitempty"`
BoardName string `json:"board_name,omitempty"`
TempSW int `json:"temp_sw,omitempty"`
Uptime string `json:"uptime,omitempty"`
UptimeVal int `json:"uptime_val,omitempty"`
UserMainStorage string `json:"user_main_storage,omitempty"`
BoxAuthenticated bool `json:"box_authenticated,omitempty"`
Serial string `json:"serial,omitempty"`
FirmwareVersion string `json:"firmware_version,omitempty"`
}
}
// https://dev.freebox.fr/sdk/os/wifi/
type wifiAccessPoint struct {
Name string `json:"name,omitempty"`
ID int `json:"id,omitempty"`
}
type wifi struct {
Success bool `json:"success"`
Result []wifiAccessPoint `json:"result,omitempty"`
}
type wifiStation struct {
Hostname string `json:"hostname,omitempty"`
MAC string `json:"mac,omitempty"`
State string `json:"state,omitempty"`
Inactive int `json:"inactive,omitempty"`
RXBytes int64 `json:"rx_bytes,omitempty"`
TXBytes int64 `json:"tx_bytes,omitempty"`
ConnectionDuration int `json:"conn_duration,omitempty"`
TXRate int64 `json:"tx_rate,omitempty"`
RXRate int64 `json:"rx_rate,omitempty"`
Signal int `json:"signal,omitempty"`
}
type wifiStations struct {
Success bool `json:"success"`
Result []wifiStation `json:"result,omitempty"`
}
type app struct {
AppID string `json:"app_id"`
AppName string `json:"app_name"`
AppVersion string `json:"app_version"`
DeviceName string `json:"device_name"`
}
type api struct {
authz string
login string
loginSession string
}
type store struct {
location string
}
type authInfo struct {
myApp app
myAPI api
myStore store
myReader *bufio.Reader
}
type postRequest struct {
method, url, header string
}
// https://dev.freebox.fr/sdk/os/vpn/
type vpnServer struct {
Success bool `json:"success"`
Result []struct {
RxBytes int64 `json:"rx_bytes,omitempty"`
Authenticated bool `json:"authenticated,omitempty"`
TxBytes int64 `json:"tx_bytes,omitempty"`
User string `json:"user,omitempty"`
ID string `json:"id,omitempty"`
Vpn string `json:"vpn,omitempty"`
SrcIP string `json:"src_ip,omitempty"`
AuthTime int32 `json:"auth_time,omitempty"`
LocalIP string `json:"local_ip,omitempty"`
} `json:"result,omitempty"`
}