Skip to content
This repository has been archived by the owner on May 21, 2022. It is now read-only.

Commit

Permalink
fetchback: code to fetch measurement from API
Browse files Browse the repository at this point in the history
We're aiming for writing a full end to end experiment where the code
performs measurements and then fetches them back from API.

However, the API code currently does not store measurements submitted by
us using ams-pg.ooni.org, so we cannot go that far.

Yet, let us have support for this functionality, which has been tested
by adapting ooni/explorer#486.

Also, for now just write basic smoke testing. The diff itself details
a plan for integrating this code into probe-engine. When we'll do that
we'll most likely want to write more testing code.

This work is part of ooni/backend#446.
  • Loading branch information
bassosimone committed Sep 22, 2020
1 parent cb1935e commit 98b587a
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/fetchback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: fetchback
on:
pull_request:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v1
with:
go-version: "1.14"
- uses: actions/checkout@v2
- run: sudo apt install jq
- run: ./script/fetchbacktest
145 changes: 145 additions & 0 deletions script/fetchback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// +build ignore
// +build forcetesting

package main

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
)

// TODO(bassosimone): most of the code in this file should be moved to
// github.com/ooni/probe-engine once we're always using the new API. When
// this happens, this code will be part of the probeservices package.

// The following errors are returned by this API.
var (
ErrEmptyReportID = errors.New("empty report ID")
)

// Config contains the configuration for fetching a measurement.
type Config struct {
// ReportID is the mandatory report ID.
ReportID string

// Full indicates whether we also want the full measurement body.
Full bool

// Input is the optional input.
Input string

// Debugf is a function called to emit debug messages.
Debugf func(format string, v ...interface{})

// Client is the optional HTTP client.
Client *http.Client

// BaseURL is the optiona base URL.
BaseURL string
}

// MeasurementMeta contains measurement metadata.
type MeasurementMeta struct {
// Fields returned by the API server whenever we are
// calling /api/v1/measurement_meta.
Anomaly bool `json:"anomaly"`
CategoryCode string `json:"category_code"`
Confirmed bool `json:"confirmed"`
Failure bool `json:"failure"`
Input *string `json:"input"`
MeasurementStartTime time.Time `json:"measurement_start_time"`
ProbeASN int64 `json:"probe_asn"`
ProbeCC string `json:"probe_cc"`
ReportID string `json:"report_id"`
Scores string `json:"scores"`
TestName string `json:"test_name"`
TestStartTime time.Time `json:"test_start_time"`

// This field is only included if the user has specified
// the full option, otherwise it's an empty string.
RawMeasurement string `json:"raw_measurement"`

// This field contains the body that we received from the
// API server and it's here to help debugging.
RawBody []byte `json:"-"`
}

// GetMeasurementMeta gets measurement metadata.
func GetMeasurementMeta(ctx context.Context, config Config) (MeasurementMeta, error) {
if config.ReportID == "" {
return MeasurementMeta{}, ErrEmptyReportID
}
if config.Debugf == nil {
config.Debugf = log.Printf
}
if config.Client == nil {
config.Client = http.DefaultClient
}
if config.BaseURL == "" {
config.BaseURL = "https://ams-pg.ooni.org"
}
URL, err := url.Parse(config.BaseURL)
if err != nil {
return MeasurementMeta{}, err
}
URL.Path = "/api/v1/measurement_meta"
query := url.Values{}
query.Add("report_id", config.ReportID)
if config.Input != "" {
query.Add("input", config.Input)
}
if config.Full {
query.Add("full", "true")
}
URL.RawQuery = query.Encode()
config.Debugf("> GET %s", URL.String())
resp, err := config.Client.Get(URL.String())
if err != nil {
return MeasurementMeta{}, err
}
config.Debugf("< %d", resp.StatusCode)
defer resp.Body.Close()
reader := io.LimitReader(resp.Body, 1<<25)
body, err := ioutil.ReadAll(reader)
if err != nil {
return MeasurementMeta{}, err
}
var mmeta MeasurementMeta
err = json.Unmarshal(body, &mmeta)
mmeta.RawBody = body // helps debugging
if err != nil {
return mmeta, err
}
return mmeta, nil
}

func fatalOnError(err error) {
if err != nil {
log.Fatal(err)
}
}

func main() {
reportid := flag.String("report-id", "", "Report ID of the measurement")
input := flag.String("input", "", "Input of the measurement")
full := flag.Bool("full", false, "Also include the measurement body")
flag.Parse()
mmeta, err := GetMeasurementMeta(context.Background(), Config{
ReportID: *reportid,
Input: *input,
Full: *full,
})
fatalOnError(err)
data, err := json.Marshal(mmeta)
fatalOnError(err)
fmt.Printf("%s\n", data)
}
16 changes: 16 additions & 0 deletions script/fetchbacktest
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
set -ex

# TODO(bassosimone): consider writing more comprehensive tests. At the same
# time consider that the backend has good coverage. We should still probably
# ensure we're getting the expected result to validate _our_ code.

# measurement from the past where the input doesn't matter
go run ./script/fetchback.go -report-id \
20200316T235941Z_AS14522_uY1btNTrufkzxq1sIQ5s4TgcEjPiTmnXTN0jdj1N64GJUVIV5e \
| jq

# measurement from the past where the input matters
go run ./script/fetchback.go -report-id \
20200316T221937Z_AS4181_rdC4mBUM3RA2Qks35LXLEhZ6ZG8Sm8TDkuXEE2tRNUkOMc0QEe \
-input http://emule.com/ | jq

0 comments on commit 98b587a

Please sign in to comment.