package main import ( "bytes" "encoding/json" "fmt" "html/template" "io/ioutil" "log" "net/http" "net/url" "os" "path/filepath" "strconv" "time" "github.com/pkg/errors" ) var httpClient *http.Client var captchaAPIURL *url.URL var captchaChallenges []string var items []string // 5 bits of difficulty, 1 in 2^6 (1 in 32) tries will succeed on average. // // 8 bits of difficulty would be ok for apps that are never used on mobile phones, 6 is better suited for mobile apps // const captchaDifficultyLevel = 5 func main() { httpClient = &http.Client{ Timeout: time.Second * time.Duration(5), } apiToken := os.ExpandEnv("$CAPTCHA_API_TOKEN") if apiToken == "" { panic(errors.New("can't start the app, the CAPTCHA_API_TOKEN environment variable is required")) } var err error captchaAPIURL, err = url.Parse("http://localhost:2370") if err != nil { panic(errors.New("can't start the app because can't parse captchaAPIURL")) } err = loadCaptchaChallenges(apiToken) if err != nil { panic(errors.Wrap(err, "can't start the app because could not loadCaptchaChallenges():")) } _, err = ioutil.ReadFile("index.html") if err != nil { panic(errors.Wrap(err, "can't start the app because can't open the template file. Are you in the right directory? ")) } http.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) { // The user submitted a POST request, attempting to add a new item to the list if request.Method == "POST" { // Ask the captcha server if the user's proof of work result is legit, // and if not, return HTTP 400 Bad Request err := request.ParseForm() if err == nil { err = validateCaptcha(apiToken, request.Form.Get("challenge"), request.Form.Get("nonce")) } if err != nil { responseWriter.WriteHeader(400) responseWriter.Write([]byte(fmt.Sprintf("400 bad request: %s", err))) return } // Validation passed, add the user's new item to the list items = append(items, request.Form.Get("item")) } // if it looks like we will run out of challenges soon, then kick off a goroutine to go get more in the background // note that in a real application in production, you would want to use a lock or mutex to ensure that // this only happens once if lots of requests come in at the same time if len(captchaChallenges) > 0 && len(captchaChallenges) < 5 { go loadCaptchaChallenges(apiToken) } // if we somehow completely ran out of challenges, load more synchronously if captchaChallenges == nil || len(captchaChallenges) == 0 { err = loadCaptchaChallenges(apiToken) if err != nil { log.Printf("loading captcha challenges failed: %v", err) responseWriter.WriteHeader(500) responseWriter.Write([]byte("captcha api error")) return } } // This gets & consumes the next challenge from the begining of the slice challenge := captchaChallenges[0] captchaChallenges = captchaChallenges[1:] // render the page HTML & output the result to the web browser htmlBytes, err := renderPageTemplate(challenge) if err != nil { log.Printf("renderPageTemplate(): %v", err) responseWriter.WriteHeader(500) responseWriter.Write([]byte("500 internal server error")) return } responseWriter.Write(htmlBytes) }) log.Println("📋 Todo List example application listening on port 8080") err = http.ListenAndServe(":8080", nil) // if got this far it means server crashed! panic(err) } func renderPageTemplate(challenge string) ([]byte, error) { // in a real application in production you would read the template file & parse it 1 time when the app starts // I'm doing it for each request here just to make it easier to hack on it while its running 😇 indexHTMLTemplateString, err := ioutil.ReadFile("index.html") if err != nil { return nil, errors.Wrap(err, "can't open the template file. Are you in the right directory? ") } pageTemplate, err := template.New("master").Parse(string(indexHTMLTemplateString)) if err != nil { return nil, errors.Wrap(err, "can't parse the template file: ") } // constructing an instance of an anonymous struct type to contain all the data // that we need to pass to the template pageData := struct { Challenge string Items []string CaptchaURL string }{ Challenge: challenge, Items: items, CaptchaURL: captchaAPIURL.String(), } var outputBuffer bytes.Buffer err = pageTemplate.Execute(&outputBuffer, pageData) if err != nil { return nil, errors.Wrap(err, "rendering page template failed: ") } return outputBuffer.Bytes(), nil } func loadCaptchaChallenges(apiToken string) error { query := url.Values{} query.Add("difficultyLevel", strconv.Itoa(captchaDifficultyLevel)) loadURL := url.URL{ Scheme: captchaAPIURL.Scheme, Host: captchaAPIURL.Host, Path: filepath.Join(captchaAPIURL.Path, "GetChallenges"), RawQuery: query.Encode(), } captchaRequest, err := http.NewRequest("POST", loadURL.String(), nil) captchaRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) if err != nil { return err } response, err := httpClient.Do(captchaRequest) if err != nil { return err } responseBytes, err := ioutil.ReadAll(response.Body) if err != nil { return err } if response.StatusCode != 200 { return fmt.Errorf( "load proof of work captcha challenges api returned http %d: %s", response.StatusCode, string(responseBytes), ) } err = json.Unmarshal(responseBytes, &captchaChallenges) if err != nil { return err } if len(captchaChallenges) == 0 { return errors.New("proof of work captcha challenges api returned empty array") } return nil } func validateCaptcha(apiToken, challenge, nonce string) error { query := url.Values{} query.Add("challenge", challenge) query.Add("nonce", nonce) verifyURL := url.URL{ Scheme: captchaAPIURL.Scheme, Host: captchaAPIURL.Host, Path: filepath.Join(captchaAPIURL.Path, "Verify"), RawQuery: query.Encode(), } captchaRequest, err := http.NewRequest("POST", verifyURL.String(), nil) captchaRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken)) if err != nil { return err } response, err := httpClient.Do(captchaRequest) if err != nil { return err } if response.StatusCode != 200 { return errors.New("proof of work captcha validation failed") } return nil }