GitLab is used only for code review, issue tracking and project management. Canonical locations for source code are still https://gitweb.torproject.org/ https://git.torproject.org/ and git-rw.torproject.org.

handlers.go 5.97 KB
Newer Older
1 2 3 4
package main

import (
	"encoding/json"
5
	"errors"
6 7 8 9
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
10
	"path"
11
	"time"
12 13

	"golang.org/x/time/rate"
14 15
)

16 17 18
var IndexPage string
var SuccessPage string
var FailurePage string
19

20 21 22
// BridgeTest represents the result of a bridge test, sent back to the client
// as JSON object.
type BridgeTest struct {
23 24 25
	Functional bool      `json:"functional"`
	LastTested time.Time `json:"last_tested"`
	Error      string    `json:"error,omitempty"`
26 27 28
}

// TestResult represents the result of a test.
29
type TestResult struct {
30 31 32
	Bridges map[string]*BridgeTest `json:"bridge_results"`
	Time    float64                `json:"time"`
	Error   string                 `json:"error,omitempty"`
33 34
}

35
// TestRequest represents a client's request to test a batch of bridges.
36
type TestRequest struct {
37
	BridgeLines []string `json:"bridge_lines"`
38
	resultChan  chan *TestResult
39 40
}

41 42 43 44
// limiter implements a rate limiter.  We allow 1 request per second on average
// with bursts of up to 5 requests per second.
var limiter = rate.NewLimiter(1, 5)

45 46 47 48 49 50 51
func NewTestResult() *TestResult {

	t := &TestResult{}
	t.Bridges = make(map[string]*BridgeTest)
	return t
}

52 53 54 55 56 57 58 59
// LoadHtmlTemplates loads all HTML templates from the given directory.
func LoadHtmlTemplates(dir string) {

	IndexPage = LoadHtmlTemplate(path.Join(dir, "index.html"))
	SuccessPage = LoadHtmlTemplate(path.Join(dir, "success.html"))
	FailurePage = LoadHtmlTemplate(path.Join(dir, "failure.html"))
}

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
// LoadHtmlTemplate reads the content of the given filename and returns it as
// string.  If the function is unable to read the file, it logs a fatal error.
func LoadHtmlTemplate(filename string) string {

	content, err := ioutil.ReadFile(filename)
	if err != nil {
		log.Fatal(err)
	}
	return string(content)
}

func SendResponse(w http.ResponseWriter, response string) {
	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, response)
}

func SendHtmlResponse(w http.ResponseWriter, response string) {

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	SendResponse(w, response)
}

func SendJSONResponse(w http.ResponseWriter, response string) {

	w.Header().Set("Content-Type", "application/json")
85
	log.Printf("Test result: %s", response)
86 87 88 89 90 91 92 93
	SendResponse(w, response)
}

func Index(w http.ResponseWriter, r *http.Request) {

	SendHtmlResponse(w, IndexPage)
}

94
func testBridgeLines(req *TestRequest) *TestResult {
95 96 97 98 99

	// Add cached bridge lines to the result.
	result := NewTestResult()
	remainingBridgeLines := []string{}
	numCached := 0
100
	for _, bridgeLine := range req.BridgeLines {
101 102
		if entry := cache.IsCached(bridgeLine); entry != nil {
			numCached++
103 104 105 106 107
			result.Bridges[bridgeLine] = &BridgeTest{
				Functional: entry.Error == "",
				LastTested: entry.Time,
				Error:      entry.Error,
			}
108 109
		} else {
			remainingBridgeLines = append(remainingBridgeLines, bridgeLine)
110 111 112
		}
	}

113 114 115 116
	// Test whatever bridges remain.
	if len(remainingBridgeLines) > 0 {
		log.Printf("%d bridge lines served from cache; testing remaining %d bridge lines.",
			numCached, len(remainingBridgeLines))
117

118
		start := time.Now()
119 120 121
		req.resultChan = make(chan *TestResult)
		torCtx.RequestQueue <- req
		partialResult := <-req.resultChan
122
		result.Time = float64(time.Now().Sub(start).Seconds())
123
		result.Error = partialResult.Error
124 125 126

		// Cache partial test results and add them to our existing result object.
		for bridgeLine, bridgeTest := range partialResult.Bridges {
127
			cache.AddEntry(bridgeLine, errors.New(bridgeTest.Error), bridgeTest.LastTested)
128 129 130 131
			result.Bridges[bridgeLine] = bridgeTest
		}
	} else {
		log.Printf("All %d bridge lines served from cache.  No need for testing.", numCached)
132
	}
133

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
	// Log fraction of bridges that are functional.
	numFunctional, numDysfunctional := 0, 0
	for _, bridgeTest := range result.Bridges {
		if bridgeTest.Functional {
			numFunctional++
		} else {
			numDysfunctional++
		}
	}
	log.Printf("Tested %d bridges: %d (%.1f%%) functional; %d (%.1f%%) dysfunctional.",
		len(result.Bridges),
		numFunctional,
		float64(numFunctional)/float64(len(result.Bridges))*100,
		numDysfunctional,
		float64(numDysfunctional)/float64(len(result.Bridges))*100)

150
	return result
151 152
}

153
func BridgeState(w http.ResponseWriter, r *http.Request) {
154

155 156 157 158 159 160 161 162 163 164 165 166 167
	b, err := ioutil.ReadAll(r.Body)
	defer r.Body.Close()
	if err != nil {
		log.Printf("Failed to read HTTP body: %s", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	req := &TestRequest{}
	if err := json.Unmarshal(b, &req); err != nil {
		log.Printf("Failed to unmarshal HTTP body %q: %s", b, err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
168 169
	}

170 171 172
	if len(req.BridgeLines) == 0 {
		log.Printf("Got request with no bridge lines.")
		http.Error(w, "no bridge lines given", http.StatusBadRequest)
173 174
		return
	}
175

176 177 178 179
	if len(req.BridgeLines) > MaxBridgesPerReq {
		log.Printf("Got %d bridges in request but we only allow <= %d.", len(req.BridgeLines), MaxBridgesPerReq)
		http.Error(w, fmt.Sprintf("maximum of %d bridge lines allowed", MaxBridgesPerReq), http.StatusBadRequest)
		return
180 181
	}

182
	log.Printf("Got %d bridge lines from %s.", len(req.BridgeLines), r.RemoteAddr)
183
	result := testBridgeLines(req)
184 185

	jsonResult, err := json.Marshal(result)
186 187 188
	if err != nil {
		log.Printf("Bug: %s", err)
		http.Error(w, "failed to marshal test tesult", http.StatusInternalServerError)
189
		return
190 191
	}
	SendJSONResponse(w, string(jsonResult))
192 193 194 195 196 197 198 199 200 201 202
}

func BridgeStateWeb(w http.ResponseWriter, r *http.Request) {

	r.ParseForm()
	// Rate-limit Web requests to prevent someone from abusing this service
	// as a port scanner.
	if limiter.Allow() == false {
		SendHtmlResponse(w, "Rate limit exceeded.")
		return
	}
203 204
	bridgeLine := r.Form.Get("bridge_line")
	if bridgeLine == "" {
205
		SendHtmlResponse(w, "No bridge line given.")
206 207
		return
	}
208

209
	result := testBridgeLines(&TestRequest{BridgeLines: []string{bridgeLine}})
210 211 212 213 214 215 216 217
	bridgeResult, exists := result.Bridges[bridgeLine]
	if !exists {
		log.Printf("Bug: Test result not part of our result map.")
		SendHtmlResponse(w, FailurePage)
		return
	}

	if bridgeResult.Functional {
218
		SendHtmlResponse(w, SuccessPage)
219
	} else {
220
		SendHtmlResponse(w, FailurePage)
221 222
	}
}