func SendRequestToAPI(apiURL, token string, w http.ResponseWriter, r *http.Request) (*http.Response, error) { l.Log(fmt.Sprintf("Sending request %v to Sierra API with token %v", apiURL, token), l.TraceMessage) req, err := http.NewRequest("GET", apiURL, nil) if err != nil { http.Error(w, "Request failed.", http.StatusInternalServerError) return new(http.Response), err } req.Close = true err = SetAuthorizationHeaders(req, r, token) if err != nil { l.Log("The remote address in an incoming request is not set properly.", l.WarnMessage) } client := &http.Client{} resp, err := client.Do(req) if err != nil { http.Error(w, "Error querying Sierra API.", http.StatusInternalServerError) return resp, err } l.Log(fmt.Sprintf("Sending response %#v back to caller", resp), l.TraceMessage) return resp, nil }
func newBibsHandler(w http.ResponseWriter, r *http.Request) { setACAOHeader(w, r, *headerACAO) entries := make(map[int]sierraapi.BibRecordOut) entries, err := getNewItems(entries, time.Now(), w, r) if err != nil { return } var response sierraapi.BibRecordsOut for _, entry := range entries { response = append(response, entry) } sort.Sort(sort.Reverse(response)) finalJSON, err := json.Marshal(response) if err != nil { http.Error(w, "JSON Encoding Error", http.StatusInternalServerError) l.Log(fmt.Sprintf("Internal Server Error at /new handler, JSON Encoding Error: %v", err), l.WarnMessage) return } l.Log(fmt.Sprintf("Sending response at /new handler: %v", response), l.TraceMessage) w.Header().Set("Content-Type", "application/json;charset=UTF-8 ") w.Write(finalJSON) }
func homeHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") if r.URL.Path != "/" { w.WriteHeader(http.StatusNotFound) fmt.Fprint(w, "<html><head></head><body><pre>404 - Not Found</pre></body></html>") l.Log("404 Handler visited.", l.TraceMessage) return } l.Log("Home Handler visited.", l.TraceMessage) fmt.Fprint(w, "<html><head></head><body><h1>Welcome to Tyro! The Sierra API helper.</h1></body></html>") }
func (t *TokenStore) refresh(tokenURL, clientKey, clientSecret string) (int, error) { type AuthTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } bodyValues := url.Values{} bodyValues.Set("grant_type", "client_credentials") getTokenRequest, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(bodyValues.Encode())) if err != nil { t.set("") l.Log(err, l.WarnMessage) return 0, err } getTokenRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded") getTokenRequest.SetBasicAuth(clientKey, clientSecret) getTokenRequest.Close = true client := new(http.Client) resp, err := client.Do(getTokenRequest) if err != nil { t.set("") l.Log(err, l.WarnMessage) return 0, err } if resp.StatusCode != 200 { t.set("") l.Log(err, l.WarnMessage) return 0, fmt.Errorf("Unable to authenticate to token generator, %v", resp.StatusCode) } var responseJSON AuthTokenResponse err = json.NewDecoder(resp.Body).Decode(&responseJSON) defer resp.Body.Close() if err != nil { t.set("") l.Log(err, l.WarnMessage) return 0, err } if responseJSON.ExpiresIn < MinimumTokenTTL { t.set("") return 0, errors.New("Token has a expire_in that is too small.") } else { l.Log("Received Token", l.TraceMessage) t.set(responseJSON.AccessToken) return responseJSON.ExpiresIn, nil } }
func getTokenOrError(w http.ResponseWriter, r *http.Request) (string, error) { token, err := tokenStore.Get() if err != nil { http.Error(w, "Token Error.", http.StatusInternalServerError) return token, err } if token == tokenstore.UninitialedTokenValue { l.Log("Waiting for token to initialize...", l.TraceMessage) select { case <-tokenStore.Initialized: tokenStore.Initialized <- struct{}{} token, err = tokenStore.Get() if err != nil { http.Error(w, "Token Error.", http.StatusInternalServerError) return token, err } case <-time.After(time.Second * 30): http.Error(w, "Token Error.", http.StatusInternalServerError) return token, errors.New("Unable to get token from TokenStore") } } return token, err }
func getNumberOfEntries(date time.Time, w http.ResponseWriter, r *http.Request) (int, error) { type totalResponse struct { Total int `json:"total"` } token, err := getTokenOrError(w, r) if err != nil { l.Log(err, l.ErrorMessage) return 0, err } parsedAPIURL, err := parseURLandJoinToPath(*apiURL, sierraapi.BibRequestEndpoint) if err != nil { http.Error(w, "Server Error.", http.StatusInternalServerError) l.Log("Internal Server Error at /new handler, unable to parse url.", l.DebugMessage) return 0, err } q := parsedAPIURL.Query() q.Set("limit", "1") q.Set("offset", "0") q.Set("deleted", "false") q.Set("suppressed", "false") q.Set("createdDate", fmt.Sprintf("[%v,%v]", date.AddDate(0, 0, -1).Format(time.RFC3339), date.Format(time.RFC3339))) q.Set("fields", "default") parsedAPIURL.RawQuery = q.Encode() resp, err := sierraapi.SendRequestToAPI(parsedAPIURL.String(), token, w, r) if err != nil { l.Log(fmt.Sprintf("Internal Server Error at /new, %v", err), l.ErrorMessage) return 0, err } if resp.StatusCode == http.StatusUnauthorized { http.Error(w, "Token is out of date, or is refreshing. Try request again.", http.StatusInternalServerError) tokenStore.Refresh <- struct{}{} l.Log("Token is out of date.", l.ErrorMessage) return 0, errors.New("Unauthorized") } var response totalResponse err = json.NewDecoder(resp.Body).Decode(&response) return response.Total, nil }
func (t *TokenStore) Get() (string, error) { t.lock.RLock() defer t.lock.RUnlock() if t.value == "" { return "", errors.New("Token generation error.") } l.Log("Sending token.", l.TraceMessage) return t.value, nil }
//This function runs forever, waiting for a timeout //or a message on the Refresh channel. It will exit if the Refresh //channel is closed. func (t *TokenStore) Refresher(tokenURL, clientKey, clientSecret string) { runRefreshSetUpNext := func() <-chan time.Time { refreshIn, err := t.refresh(tokenURL, clientKey, clientSecret) if err != nil { l.Log(err, l.ErrorMessage) refreshIn = DefaultRefreshTime + TokenRefreshBuffer } futureTime := refreshIn - TokenRefreshBuffer lm := fmt.Sprintf("%v seconds in the future, a refresh will happen.", futureTime) l.Log(lm, l.TraceMessage) return time.After(time.Duration(futureTime) * time.Second) } refreshOrTimeout := func(timeout <-chan time.Time) (<-chan time.Time, error) { select { case <-timeout: l.Log("The old token timed out.", l.TraceMessage) return runRefreshSetUpNext(), nil case _, ok := <-t.Refresh: if ok { l.Log("A new token has been requested", l.TraceMessage) return runRefreshSetUpNext(), nil } else { return make(<-chan time.Time), errors.New("Refresh channel is closed.") } } } go func() { toc := runRefreshSetUpNext() err := errors.New("") for { toc, err = refreshOrTimeout(toc) if err != nil { return } } }() }
func rawRewriter(r *http.Request) { token, err := tokenStore.Get() if err != nil { l.Log("Error at /raw/ handler, token not yet generated.", l.DebugMessage) } parsedAPIURL, err := parseURLandJoinToPath(*apiURL, r.URL.Path[len("/raw/"):]) if err != nil { log.Fatalf("FATAL: %v", err) } parsedAPIURL.RawQuery = r.URL.RawQuery r.URL = parsedAPIURL err = sierraapi.SetAuthorizationHeaders(r, r, token) if err != nil { l.Log("The remote address in an incoming request is not set properly", l.DebugMessage) } l.Log(fmt.Sprintf("Sending proxied request: %v", r), l.TraceMessage) }
func main() { flag.Parse() l.Set(l.ParseLogLevel(*logLevel)) overrideUnsetFlagsFromEnvironmentVariables() l.SetupLumberjack( *logFileLocation, *logMaxSize, *logMaxBackups, *logMaxAge) l.Log("Starting Tyro", l.InfoMessage) l.Log("Serving on address: "+*address, l.InfoMessage) l.Log("Using Client Key: "+*clientKey, l.InfoMessage) l.Log("Using Client Secret: "+*clientSecret, l.InfoMessage) l.Log("Connecting to API URL: "+*apiURL, l.InfoMessage) l.Log("Using ACAO header: "+*headerACAO, l.InfoMessage) l.Log(fmt.Sprintf("Allowing access to raw Sierra API: %v", *raw), l.InfoMessage) if *clientKey == "" { log.Fatal("FATAL: A client key is required to authenticate against the Sierra API.") } else if *clientSecret == "" { log.Fatal("FATAL: A client secret is required to authenticate against the Sierra API.") } if *headerACAO == "*" { l.Log("Using \"*\" for \"Access-Control-Allow-Origin\" header. API will be public!", l.WarnMessage) } if *certFile != "" { l.Log("Going to try to serve through HTTPS", l.InfoMessage) l.Log("Using Certificate File: "+*certFile, l.InfoMessage) l.Log("Using Private Key File: "+*keyFile, l.InfoMessage) } parsedURL, err := parseURLandJoinToPath(*apiURL, sierraapi.TokenRequestEndpoint) if err != nil { log.Fatal("FATAL: Unable to parse API URL.") } tokenStore.Refresher(parsedURL.String(), *clientKey, *clientSecret) defer close(tokenStore.Refresh) http.HandleFunc("/", homeHandler) http.HandleFunc("/status/", statusHandler) http.HandleFunc("/status/item/", statusItemHandler) http.HandleFunc("/status/bib/", statusBibHandler) http.HandleFunc("/new", newBibsHandler) if *raw { l.Log("Allowing access to raw Sierra API.", l.WarnMessage) rawProxy := httputil.NewSingleHostReverseProxy(&url.URL{}) rawProxy.Director = rawRewriter http.Handle("/raw/", rawProxy) } if *certFile == "" { log.Fatalf("FATAL: %v", http.ListenAndServe(*address, nil)) } else { //Remove SSL 3.0 compatibility for POODLE exploit mitigation config := &tls.Config{MinVersion: tls.VersionTLS10} server := &http.Server{Addr: *address, Handler: nil, TLSConfig: config} log.Fatalf("FATAL: %v", server.ListenAndServeTLS(*certFile, *keyFile)) } }
func getNewItems(alreadyProcessed map[int]sierraapi.BibRecordOut, date time.Time, w http.ResponseWriter, r *http.Request) (map[int]sierraapi.BibRecordOut, error) { token, err := getTokenOrError(w, r) if err != nil { l.Log(err, l.ErrorMessage) return nil, err } parsedAPIURL, err := parseURLandJoinToPath(*apiURL, sierraapi.BibRequestEndpoint) if err != nil { http.Error(w, "Server Error.", http.StatusInternalServerError) l.Log("Internal Server Error at /new handler, unable to parse url.", l.DebugMessage) return nil, err } total, err := getNumberOfEntries(date, w, r) if err != nil { return nil, err } offset := 0 needOneMoreDay := false if total >= *newLimit { offset = total - *newLimit } else { needOneMoreDay = true } q := parsedAPIURL.Query() q.Set("offset", strconv.Itoa(offset)) q.Set("deleted", "false") q.Set("createdDate", fmt.Sprintf("[%v,%v]", date.AddDate(0, 0, -1).Format(time.RFC3339), date.Format(time.RFC3339))) q.Set("fields", "marc,default") q.Set("suppressed", "false") parsedAPIURL.RawQuery = q.Encode() resp, err := sierraapi.SendRequestToAPI(parsedAPIURL.String(), token, w, r) if err != nil { l.Log(fmt.Sprintf("Internal Server Error at /new, %v", err), l.ErrorMessage) return nil, err } if resp.StatusCode == http.StatusUnauthorized { http.Error(w, "Token is out of date, or is refreshing. Try request again.", http.StatusInternalServerError) tokenStore.Refresh <- struct{}{} l.Log("Token is out of date.", l.ErrorMessage) return nil, err } var response sierraapi.BibRecordsIn err = json.NewDecoder(resp.Body).Decode(&response) defer resp.Body.Close() if err != nil { http.Error(w, "JSON Decoding Error", http.StatusInternalServerError) l.Log(fmt.Sprintf("Internal Server Error at /new handler, JSON Decoding Error: %v", err), l.WarnMessage) return nil, err } entries := response.Convert() for _, entry := range *entries { alreadyProcessed[entry.BibID] = entry } if needOneMoreDay { return getNewItems(alreadyProcessed, date.Add(time.Duration(1435)*time.Minute*-1), w, r) } else { return alreadyProcessed, nil } }
func statusBibHandler(w http.ResponseWriter, r *http.Request) { setACAOHeader(w, r, *headerACAO) token, err := getTokenOrError(w, r) if err != nil { l.Log(err, l.ErrorMessage) return } bibID := strings.Split(r.URL.Path[len("/status/bib/"):], "/")[0] if bibID == "" { http.Error(w, "Error, you need to provide a BibID. /status/bib/[BidID]", http.StatusBadRequest) l.Log("Bad Request at /status/bib/ handler, no BidID provided.", l.TraceMessage) return } parsedAPIURL, err := parseURLandJoinToPath(*apiURL, sierraapi.ItemRequestEndpoint) if err != nil { http.Error(w, "Server Error.", http.StatusInternalServerError) l.Log("Internal Server Error at /status/bib/ handler, unable to parse url.", l.DebugMessage) return } q := parsedAPIURL.Query() q.Set("bibIds", bibID) q.Set("deleted", "false") q.Set("suppressed", "false") parsedAPIURL.RawQuery = q.Encode() resp, err := sierraapi.SendRequestToAPI(parsedAPIURL.String(), token, w, r) if err != nil { l.Log(fmt.Sprintf("Internal Server Error at /status/bib/, %v", err), l.ErrorMessage) return } if resp.StatusCode == http.StatusUnauthorized { http.Error(w, "Token is out of date, or is refreshing. Try request again.", http.StatusInternalServerError) tokenStore.Refresh <- struct{}{} l.Log("Token is out of date.", l.ErrorMessage) return } if resp.StatusCode == http.StatusNotFound { http.Error(w, "No item records for that BibID.", http.StatusNotFound) l.Log(fmt.Sprintf("No items records match BibID %v", bibID), l.TraceMessage) return } var responseJSON sierraapi.ItemRecordsIn err = json.NewDecoder(resp.Body).Decode(&responseJSON) defer resp.Body.Close() if err != nil { http.Error(w, "JSON Decoding Error", http.StatusInternalServerError) l.Log(fmt.Sprintf("Internal Server Error at /status/bib/ handler, JSON Decoding Error: %v", err), l.WarnMessage) return } finalJSON, err := json.Marshal(responseJSON.Convert()) if err != nil { http.Error(w, "JSON Encoding Error", http.StatusInternalServerError) l.Log(fmt.Sprintf("Internal Server Error at /status/bib/ handler, JSON Encoding Error: %v", err), l.WarnMessage) return } l.Log(fmt.Sprintf("Sending response at /status/bib/ handler: %v", responseJSON.Convert()), l.TraceMessage) w.Header().Set("Content-Type", "application/json;charset=UTF-8") w.Write(finalJSON) }
func statusHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusBadRequest) l.Log("Bare Status Handler visited.", l.TraceMessage) fmt.Fprint(w, "<html><head></head><body><pre>Available endpoints: /status/bib/[bibID] and /status/item/[itemID]</pre></body></html>") }