blob: 956c67dde5d69e55b1aacfbafaf2fa866f90c2ed [file]
// 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.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type ErrBadResponse struct {
Code int
}
func (e *ErrBadResponse) Error() string {
return fmt.Sprintf("http code %d", e.Code)
}
var (
ExpectedHeaders = map[string]string{
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
}
)
func makeCorsHeaders(method string) map[string]string {
return map[string]string{
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Access-Control-Request-Method": method,
"Access-Control-Request-Headers": "authorization",
"Referer": "http://localhost:40001/",
"Origin": "http://localhost:40001",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"TE": "trailers",
}
}
func verifyServerHeaders(header http.Header) error {
for k, v := range ExpectedHeaders {
if actual := header.Get(k); actual != v {
return fmt.Errorf("header %s mismatch: %s (expected %s)", k, actual, v)
}
}
return nil
}
func GetSdkList(url string) (SdkList, error) {
var result SdkList
err := Get(&result, url, nil, nil)
return result, err
}
func GetContentTree(url, sdk string) (ContentTree, error) {
var result ContentTree
err := Get(&result, url, map[string]string{"sdk": sdk}, nil)
return result, err
}
func GetUnitContent(url, sdk, unitId string) (Unit, error) {
var result Unit
err := Get(&result, url, map[string]string{"sdk": sdk, "id": unitId}, nil)
return result, err
}
func GetUserProgress(url, sdk, token string) (SdkProgress, error) {
var result SdkProgress
err := Get(&result, url, map[string]string{"sdk": sdk},
map[string]string{"Authorization": "Bearer " + token})
return result, err
}
func PostUnitComplete(url, sdk, unitId, token string) (ErrorResponse, error) {
var result ErrorResponse
err := Do(&result, http.MethodPost, url, map[string]string{"sdk": sdk, "id": unitId},
map[string]string{"Authorization": "Bearer " + token}, nil)
return result, err
}
func PostUserCode(url, sdk, unitId, token string, body UserCodeRequest) (ErrorResponse, error) {
raw, err := json.Marshal(body)
if err != nil {
return ErrorResponse{}, err
}
headers := map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
}
queryParams := map[string]string{"sdk": sdk, "id": unitId}
var result ErrorResponse
err = Post(&result, url, queryParams, headers, bytes.NewReader(raw))
return result, err
}
func PostDeleteProgress(url, token string) (ErrorResponse, error) {
var result ErrorResponse
err := Do(&result, http.MethodPost, url, nil,
map[string]string{"Authorization": "Bearer " + token}, nil)
return result, err
}
func Post(dst interface{}, url string, queryParams, headers map[string]string, body io.Reader) error {
if err := Options(http.MethodPost, url, queryParams); err != nil {
return fmt.Errorf("pre-flight request error: %w", err)
}
return Do(dst, http.MethodPost, url, queryParams, headers, body)
}
func Get(dst interface{}, url string, queryParams, headers map[string]string) error {
if err := Options(http.MethodGet, url, queryParams); err != nil {
return fmt.Errorf("pre-flight request error: %w", err)
}
return Do(dst, http.MethodGet, url, queryParams, headers, nil)
}
func Options(method, url string, queryParams map[string]string) error {
optionsHeaders := makeCorsHeaders(method)
return Do(nil, http.MethodOptions, url, queryParams, optionsHeaders, nil)
}
// Generic HTTP call wrapper
// params:
// * dst: response struct pointer
// * url: request url
// * query_params: url query params, as a map (we don't use multiple-valued params)
// * headers: client headers as a map
// * body: as io.Reader interface
func Do(dst interface{}, method, url string, queryParams, headers map[string]string, body io.Reader) error {
req, err := http.NewRequest(method, url, body)
if err != nil {
return err
}
for k, v := range headers {
req.Header.Add(k, v)
}
if len(queryParams) > 0 {
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if err := verifyServerHeaders(resp.Header); err != nil {
return err
}
if method == http.MethodOptions {
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("options request failed, http code %d", resp.StatusCode)
}
// don't proceed to json body decoding, there's none
return nil
}
tee := io.TeeReader(resp.Body, os.Stdout)
defer os.Stdout.WriteString("\n")
if err := json.NewDecoder(tee).Decode(dst); err != nil {
return fmt.Errorf("response decode err: %w", err)
}
if resp.StatusCode != http.StatusOK {
return &ErrBadResponse{resp.StatusCode}
}
return nil
}