-Added API Server to lbry.go
-Removed dependency on internal apis -Moved over only required packages. -Adjusted slack.go to be generic instead of hard coding channel name. -Moved over travis package from internal-apis -Added Repository struct for webhook and an IsMatch method. It is possible for any repository to send a webhook to the api and it will trigger a deploy. We should check against the owner, repo and branch. -Renamed package to api -removed util.Debugging from server.go -Added an ErrorHandling function that be used as interface for slack for internal-apis -Added Map for Header settings that can be set before the serving -Merged slack code from lbryio/boardbot -Cleaned up the slack.go code so it made more sense and flowed better -Removed gitignore entry for `.idea`, should be global -Removed debugging.go -Added option for private vs public repository for getting travis public key. -separated private vs public into if else. -Changed HeaderSettings to not be pointer. -Changed ErrorHandler to be named LogErrorFunc -removed logrus dependency, created loginfo function to handle non-error information. -Added Daemon Types and adjusted peer_list to be in line with v20 -Fixed rpcclient library usage for latest version to prevent build errors. -Changed inputs to LogError and LogInfo so that other implementations can make this customizable.
This commit is contained in:
parent
2a6ea528bd
commit
afffa668a9
8 changed files with 649 additions and 16 deletions
api
290
api/server.go
Normal file
290
api/server.go
Normal file
|
@ -0,0 +1,290 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lbryio/lbry.go/errors"
|
||||
"github.com/lbryio/lbry.go/util"
|
||||
"github.com/lbryio/lbry.go/validator"
|
||||
v "github.com/lbryio/ozzo-validation"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
const authTokenParam = "auth_token"
|
||||
|
||||
// Server HTTP Header Settings. Set on header if exists
|
||||
// ie. "Content-Type" - "application/json; charset=utf-8"
|
||||
// ie. "X-Content-Type-Options" - "nosniff"
|
||||
// ie. "X-Frame-Options" - "deny"
|
||||
// ie."Content-Security-Policy" - "default-src 'none'"
|
||||
// ie. "X-XSS-Protection" - "1; mode=block"
|
||||
// ie. "Server" - "lbry.io"
|
||||
// ie. "Referrer-Policy" - "same-origin"
|
||||
// ie. "Strict-Transport-Security" - "max-age=31536000; preload"
|
||||
// ie. "Access-Control-Allow-Origin" -"<header.Origin>"
|
||||
// ie. "Access-Control-Allow-Methods" - "GET, POST, OPTIONS"
|
||||
var HeaderSettings map[string]string
|
||||
|
||||
// LogError Allows specific error logging for the server at specific points.
|
||||
var LogError = func(*http.Request, *Response, error) {}
|
||||
|
||||
// LogInfo Allows for specific logging information.
|
||||
var LogInfo = func(*http.Request, *Response) {}
|
||||
|
||||
// TraceEnabled Attaches a trace field to the JSON response when enabled.
|
||||
var TraceEnabled = false
|
||||
|
||||
var errAuthenticationRequired = errors.Base("authentication required")
|
||||
var errNotAuthenticated = errors.Base("could not authenticate user")
|
||||
var errForbidden = errors.Base("you are not authorized to perform this action")
|
||||
|
||||
// StatusError represents an error with an associated HTTP status code.
|
||||
type StatusError struct {
|
||||
Status int
|
||||
Err error
|
||||
}
|
||||
|
||||
// Allows StatusError to satisfy the error interface.
|
||||
func (se StatusError) Error() string {
|
||||
return se.Err.Error()
|
||||
}
|
||||
|
||||
// Response is returned by API handlers
|
||||
type Response struct {
|
||||
Status int
|
||||
Data interface{}
|
||||
RedirectURL string
|
||||
Error error
|
||||
}
|
||||
|
||||
// Handler handles API requests
|
||||
type Handler func(r *http.Request) Response
|
||||
|
||||
func (h Handler) callHandlerSafely(r *http.Request) (rsp Response) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = errors.Err("%v", r)
|
||||
}
|
||||
rsp = Response{Error: errors.Wrap(err, 2)}
|
||||
}
|
||||
}()
|
||||
|
||||
return h(r)
|
||||
}
|
||||
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Set header settings
|
||||
if HeaderSettings != nil {
|
||||
//Multiple readers, no writers is okay
|
||||
for key, value := range HeaderSettings {
|
||||
w.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop here if its a preflighted OPTIONS request
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
rsp := h.callHandlerSafely(r)
|
||||
|
||||
if rsp.Status == 0 {
|
||||
if rsp.Error != nil {
|
||||
if statusError, ok := rsp.Error.(StatusError); ok {
|
||||
rsp.Status = statusError.Status
|
||||
} else if errors.Is(rsp.Error, errAuthenticationRequired) {
|
||||
rsp.Status = http.StatusUnauthorized
|
||||
} else if errors.Is(rsp.Error, errNotAuthenticated) || errors.Is(rsp.Error, errForbidden) {
|
||||
rsp.Status = http.StatusForbidden
|
||||
} else {
|
||||
rsp.Status = http.StatusInternalServerError
|
||||
}
|
||||
} else if rsp.RedirectURL != "" {
|
||||
rsp.Status = http.StatusFound
|
||||
} else {
|
||||
rsp.Status = http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
success := rsp.Status < http.StatusBadRequest
|
||||
|
||||
consoleText := r.RemoteAddr + " [" + strconv.Itoa(rsp.Status) + "]: " + r.Method + " " + r.URL.Path
|
||||
if success {
|
||||
LogInfo(r, &rsp)
|
||||
} else {
|
||||
LogError(r, &rsp, errors.Base(consoleText))
|
||||
}
|
||||
|
||||
// redirect
|
||||
if rsp.Status >= http.StatusMultipleChoices && rsp.Status < http.StatusBadRequest {
|
||||
http.Redirect(w, r, rsp.RedirectURL, rsp.Status)
|
||||
return
|
||||
} else if rsp.RedirectURL != "" {
|
||||
LogError(r, &rsp, errors.Base("status code "+strconv.Itoa(rsp.Status)+
|
||||
" does not indicate a redirect, but RedirectURL is non-empty '"+
|
||||
rsp.RedirectURL+"'"))
|
||||
}
|
||||
|
||||
var errorString *string
|
||||
if rsp.Error != nil {
|
||||
errorStringRaw := rsp.Error.Error()
|
||||
errorString = &errorStringRaw
|
||||
}
|
||||
|
||||
var trace []string
|
||||
if TraceEnabled && errors.HasTrace(rsp.Error) {
|
||||
trace = strings.Split(errors.Trace(rsp.Error), "\n")
|
||||
for index, element := range trace {
|
||||
if strings.HasPrefix(element, "\t") {
|
||||
trace[index] = strings.Replace(element, "\t", " ", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// http://choly.ca/post/go-json-marshalling/
|
||||
jsonResponse, err := json.MarshalIndent(&struct {
|
||||
Success bool `json:"success"`
|
||||
Error *string `json:"error"`
|
||||
Data interface{} `json:"data"`
|
||||
Trace []string `json:"_trace,omitempty"`
|
||||
}{
|
||||
Success: success,
|
||||
Error: errorString,
|
||||
Data: rsp.Data,
|
||||
Trace: trace,
|
||||
}, "", " ")
|
||||
if err != nil {
|
||||
LogError(r, &rsp, errors.Prefix("Error encoding JSON response: ", err))
|
||||
}
|
||||
|
||||
if rsp.Status >= http.StatusInternalServerError {
|
||||
LogError(r, &rsp, errors.Prefix(r.Method+" "+r.URL.Path+"\n", rsp.Error))
|
||||
}
|
||||
|
||||
w.WriteHeader(rsp.Status)
|
||||
w.Write(jsonResponse)
|
||||
}
|
||||
|
||||
func FormValues(r *http.Request, params interface{}, validationRules []*v.FieldRules) error {
|
||||
ref := reflect.ValueOf(params)
|
||||
if !ref.IsValid() || ref.Kind() != reflect.Ptr || ref.Elem().Kind() != reflect.Struct {
|
||||
return errors.Err("'params' must be a pointer to a struct")
|
||||
}
|
||||
|
||||
structType := ref.Elem().Type()
|
||||
structValue := ref.Elem()
|
||||
fields := map[string]bool{}
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
name := structType.Field(i).Name
|
||||
underscoredName := util.Underscore(name)
|
||||
value := strings.TrimSpace(r.FormValue(underscoredName))
|
||||
|
||||
// if param is not set at all, continue
|
||||
// comes after call to r.FormValue so form values get parsed internally (if they arent already)
|
||||
if len(r.Form[underscoredName]) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields[underscoredName] = true
|
||||
isPtr := false
|
||||
var finalValue reflect.Value
|
||||
|
||||
structField := structValue.FieldByName(name)
|
||||
structFieldKind := structField.Kind()
|
||||
if structFieldKind == reflect.Ptr {
|
||||
isPtr = true
|
||||
structFieldKind = structField.Type().Elem().Kind()
|
||||
}
|
||||
|
||||
switch structFieldKind {
|
||||
case reflect.String:
|
||||
finalValue = reflect.ValueOf(value)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
castVal, err := cast.ToInt64E(value)
|
||||
if err != nil {
|
||||
return errors.Err("%s: must be an integer", underscoredName)
|
||||
}
|
||||
switch structFieldKind {
|
||||
case reflect.Int:
|
||||
finalValue = reflect.ValueOf(int(castVal))
|
||||
case reflect.Int8:
|
||||
finalValue = reflect.ValueOf(int8(castVal))
|
||||
case reflect.Int16:
|
||||
finalValue = reflect.ValueOf(int16(castVal))
|
||||
case reflect.Int32:
|
||||
finalValue = reflect.ValueOf(int32(castVal))
|
||||
case reflect.Int64:
|
||||
finalValue = reflect.ValueOf(castVal)
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
castVal, err := cast.ToUint64E(value)
|
||||
if err != nil {
|
||||
return errors.Err("%s: must be an unsigned integer", underscoredName)
|
||||
}
|
||||
switch structFieldKind {
|
||||
case reflect.Uint:
|
||||
finalValue = reflect.ValueOf(uint(castVal))
|
||||
case reflect.Uint8:
|
||||
finalValue = reflect.ValueOf(uint8(castVal))
|
||||
case reflect.Uint16:
|
||||
finalValue = reflect.ValueOf(uint16(castVal))
|
||||
case reflect.Uint32:
|
||||
finalValue = reflect.ValueOf(uint32(castVal))
|
||||
case reflect.Uint64:
|
||||
finalValue = reflect.ValueOf(castVal)
|
||||
}
|
||||
case reflect.Bool:
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if !validator.IsBoolString(value) {
|
||||
return errors.Err("%s: must be one of the following values: %s",
|
||||
underscoredName, strings.Join(validator.GetBoolStringValues(), ", "))
|
||||
}
|
||||
finalValue = reflect.ValueOf(validator.IsTruthy(value))
|
||||
default:
|
||||
return errors.Err("field %s is an unsupported type", name)
|
||||
}
|
||||
|
||||
if isPtr {
|
||||
if structField.IsNil() {
|
||||
structField.Set(reflect.New(structField.Type().Elem()))
|
||||
}
|
||||
structField.Elem().Set(finalValue)
|
||||
} else {
|
||||
structField.Set(finalValue)
|
||||
}
|
||||
}
|
||||
|
||||
var extraParams []string
|
||||
for k := range r.Form {
|
||||
if _, ok := fields[k]; !ok && k != authTokenParam { //TODO: fix this AUTH_PARAM hack
|
||||
extraParams = append(extraParams, k)
|
||||
}
|
||||
}
|
||||
if len(extraParams) > 0 {
|
||||
return errors.Err("Extraneous params: " + strings.Join(extraParams, ", "))
|
||||
}
|
||||
|
||||
if len(validationRules) > 0 {
|
||||
validationErr := v.ValidateStruct(params, validationRules...)
|
||||
if validationErr != nil {
|
||||
return errors.Err(validationErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue