diff --git a/src/glide.lock b/src/glide.lock new file mode 100644 index 00000000..d978dfc7 --- /dev/null +++ b/src/glide.lock @@ -0,0 +1,111 @@ +hash: 07931599e3a6477a3bc52ecf5deedf3c17d71f6665f332d607f4a365a1386fb0 +updated: 2017-09-12T14:41:24.503934812-07:00 +imports: +- name: github.com/c9s/goprocinfo + version: 19cb9f127a9c8d2034cf59ccb683cdb94b9deb6c + subpackages: + - linux +- name: github.com/Comcast/webpa-common + version: cb246362b61aaf042a936fa615a48554d111f9b4 + subpackages: + - concurrent + - health + - logging + - resource + - secure + - secure/handler + - secure/key + - server + - types + - wrp +- name: github.com/davecgh/go-spew + version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 + subpackages: + - spew +- name: github.com/fsnotify/fsnotify + version: 4da3e2cfbabc9f751898f250b49f2439785783a1 +- name: github.com/go-kit/kit + version: a9ca6725cbbea455e61c6bc8a1ed28e81eb3493b + subpackages: + - log + - log/level +- name: github.com/go-logfmt/logfmt + version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 +- name: github.com/go-stack/stack + version: 817915b46b97fd7bb80e8ab6b69f01a53ac3eebf +- name: github.com/gorilla/context + version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 +- name: github.com/gorilla/mux + version: 392c28fe23e1c45ddba891b0320b3b5df220beea +- name: github.com/hashicorp/hcl + version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca + subpackages: + - hcl/ast + - hcl/parser + - hcl/scanner + - hcl/strconv + - hcl/token + - json/parser + - json/scanner + - json/token +- name: github.com/jtacoma/uritemplates + version: 307ae868f90f4ee1b73ebe4596e0394237dacce8 +- name: github.com/justinas/alice + version: 1051eaf52fcafdd87ead59d28b065f1fcb8274ec +- name: github.com/kr/logfmt + version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 +- name: github.com/magiconair/properties + version: be5ece7dd465ab0765a9682137865547526d1dfb +- name: github.com/mitchellh/mapstructure + version: d0303fe809921458f417bcf828397a65db30a7e4 +- name: github.com/pelletier/go-toml + version: 69d355db5304c0f7f809a2edc054553e7142f016 +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib +- name: github.com/SermoDigital/jose + version: f6df55f235c24f236d11dbcf665249a59ac2021f + subpackages: + - crypto + - jws + - jwt +- name: github.com/spf13/afero + version: 9be650865eab0c12963d8753212f4f9c66cdcf12 + subpackages: + - mem +- name: github.com/spf13/cast + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 +- name: github.com/spf13/jwalterweatherman + version: 0efa5202c04663c757d84f90f5219c1250baf94f +- name: github.com/spf13/pflag + version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 +- name: github.com/spf13/viper + version: 25b30aa063fc18e48662b86996252eabdcf2f0c7 +- name: github.com/stretchr/objx + version: cbeaeb16a013161a98496fad62933b1d21786672 +- name: github.com/stretchr/testify + version: 890a5c3458b43e6104ff5da8dfa139d013d77544 + subpackages: + - assert + - linux + - mock + - require +- name: github.com/ugorji/go + version: d23841a297e5489e787e72fceffabf9d2994b52a + subpackages: + - codec +- name: golang.org/x/sys + version: e312636bdaa2fac4f0acde9d17ab9fbad2b4ad10 + subpackages: + - unix +- name: golang.org/x/text + version: 3bd178b88a8180be2df394a1fbb81313916f0e7b + subpackages: + - transform + - unicode/norm +- name: gopkg.in/natefinch/lumberjack.v2 + version: a96e63847dc3c67d17befa69c303767e2f84e54f +- name: gopkg.in/yaml.v2 + version: 25c4ec802a7d637f88d584ab26798e94ad14c13b +testImports: [] diff --git a/src/glide.yaml b/src/glide.yaml index 789569c7..ec86231b 100644 --- a/src/glide.yaml +++ b/src/glide.yaml @@ -3,4 +3,3 @@ homepage: https://github.com/Comcast/tr1d1um import: - package: github.com/Comcast/webpa-common version: cb246362b61aaf042a936fa615a48554d111f9b4 - diff --git a/src/tr1d1um/WDMP_Type.go b/src/tr1d1um/WDMP_Type.go new file mode 100644 index 00000000..a77f9b4a --- /dev/null +++ b/src/tr1d1um/WDMP_Type.go @@ -0,0 +1,70 @@ +package main + +const( + COMMAND_GET = "GET" + COMMAND_GET_ATTRS = "GET_ATTRIBUTES" + COMMAND_SET = "SET" + COMMAND_SET_ATTRS = "SET_ATTRIBUTES" + COMMAND_TEST_SET = "TEST_AND_SET" + COMMAND_ADD_ROW = "ADD_ROW" + COMMAND_DELETE_ROW = "DELETE_ROW" + COMMAND_REPLACE_ROWS = "REPLACE_ROWS" + + HEADER_WPA_SYNC_OLD_CID = "X-Webpa-Sync-Old-Cid" + HEADER_WPA_SYNC_NEW_CID = "X-Webpa-Sync-New-Cid" + HEADER_WPA_SYNC_CMC = "X-Webpa-Sync-Cmc" + + ERR_UNSUCCESSFUL_DATA_PARSE = "Unsuccessful Data Parse" +) +/* + GET-Flavored structs +*/ + +type GetWDMP struct { + Command string `json:"command"` + Names []string `json:"names,omitempty"` + Attribute string `json:"attributes,omitempty"` +} + +/* + SET-Flavored structs +*/ + +type Attr map[string]interface{} + +type SetParam struct { + Name* string `json:"name"` + DataType* int32 `json:"dataType,omitempty"` + Value interface{} `json:"value,omitempty"` + Attributes Attr `json:"attributes,omitempty"` +} + +type SetWDMP struct { + Command string `json:"command"` + OldCid string `json:"old-cid,omitempty"` + NewCid string `json:"new-cid,omitempty"` + SyncCmc string `json:"sync-cmc,omitempty"` + Parameters []SetParam `json:"parameters,omitempty"` +} + +/* + Row-related command structs +*/ + +type AddRowWDMP struct { + Command string `json:"command"` + Table string `json:"table"` + Row map[string]string `json:"row"` +} + +type ReplaceRowsWDMP struct { + Command string `json:"command"` + Table string `json:"table"` + Rows map[string]map[string]string `json:"rows"` +} + +type DeleteRowWDMP struct { + Command string `json:"command"` + Row string `json:"row"` +} + diff --git a/src/tr1d1um/conversion_utils.go b/src/tr1d1um/conversion_utils.go new file mode 100644 index 00000000..4e40801d --- /dev/null +++ b/src/tr1d1um/conversion_utils.go @@ -0,0 +1,214 @@ +package main + +import ( + "encoding/json" + "strings" + "net/http" + "io" + "errors" +) + +var ( + ErrJsonEmpty = errors.New("JSON payload is empty") +) + +type BodyReader func(io.Reader)([]byte,error) + +type Vars map[string]string + +/* The following functions break down the different cases for requests (https://swagger.webpa.comcast.net/) + containing WDMP content. Their main functionality is to attempt at reading such content, validating it + and subsequently returning a json type encoding of it. Most often this resulting []byte is used as payload for + wrp messages +*/ +func GetFlavorFormat(req *http.Request, attr, namesKey, sep string) (payload[]byte, err error){ + wdmp := new(GetWDMP) + + if nameGroup := req.FormValue(namesKey); nameGroup != "" { + wdmp.Command = COMMAND_GET + wdmp.Names = strings.Split(nameGroup, sep) + } else{ + err = errors.New("names is a required property for GET") + return + } + + if attributes := req.FormValue(attr); attributes != "" { + wdmp.Command = COMMAND_GET_ATTRS + wdmp.Attribute = attributes + } + + payload, err = json.Marshal(wdmp) + return +} + +func SetFlavorFormat(req *http.Request, ReadEntireBody BodyReader) (payload[]byte, err error){ + wdmp := new(SetWDMP) + DecodeJsonPayload(req.Body, wdmp, ReadEntireBody) + + wdmp.Command, err = ValidateAndRetrieveCommand(req.Header, wdmp) + + if err != nil { + return + } + + payload, err = json.Marshal(wdmp) + return +} + +func DeleteFlavorFormat(urlVars Vars, rowKey string) (payload[]byte, err error){ + wdmp := &DeleteRowWDMP{Command:COMMAND_DELETE_ROW} + + if row, exists := GetFromUrlPath(rowKey, urlVars); exists { + wdmp.Row = row + } else { + err = errors.New("row name is required") + return + } + + payload, err = json.Marshal(wdmp) + return +} + +func AddFlavorFormat(body io.Reader, urlVars Vars, tableName string, ReadEntireBody BodyReader) (payload[]byte, err error){ + wdmp := &AddRowWDMP{Command:COMMAND_ADD_ROW} + + if table, exists := GetFromUrlPath(tableName, urlVars); exists { + wdmp.Table = table + } else { + err = errors.New("tableName is required for this method") + return + } + err = DecodeJsonPayload(body, wdmp.Row, ReadEntireBody) + + if err != nil { + return + } + + if len(wdmp.Row) == 0 { + err = errors.New("input data is empty") + } + + payload, err = json.Marshal(wdmp) + return +} + +func ReplaceFlavorFormat(body io.Reader, urlVars Vars, tableName string, ReadEntireBody BodyReader) (payload[]byte, err error){ + wdmp := ReplaceRowsWDMP{Command:COMMAND_REPLACE_ROWS} + + if table, exists := GetFromUrlPath(tableName, urlVars); exists { + wdmp.Table = table + } else { + err = errors.New("tableName is required for this method") + return + } + + err = DecodeJsonPayload(body, &wdmp.Rows, ReadEntireBody) + + if err != nil { + return + } + + if !ValidREPLACEParams(wdmp.Rows){ + err = errors.New("invalid Replacement data") + return + } + + payload, err = json.Marshal(wdmp) + return +} + + +/* Validation functions */ +func ValidateAndRetrieveCommand(header http.Header, wdmp *SetWDMP) (command string, err error){ + if newCid := header.Get(HEADER_WPA_SYNC_NEW_CID); newCid != "" { + wdmp.OldCid = header.Get(HEADER_WPA_SYNC_OLD_CID) + wdmp.NewCid = newCid + wdmp.SyncCmc = header.Get(HEADER_WPA_SYNC_CMC) + command, err = ValidateSETParams(false, wdmp, COMMAND_TEST_SET) + } else { + command, err = ValidateSETParams(true, wdmp, "") + } + return +} + +// -Inputs-: +// **checkingForSetAttr**: true if we're checking for the required parameter properties for the SET_ATTRIBUTES command +// These properties are: attributes and name +// +// **wdmp**: the WDMP object from which we retrieve the parameters +// +// **override**: overrides the final suggested command if non-empty. Useful if one just wants to check for SET command +// parameter properties (value, dataType, name) +// +// -Outputs-: +// *command**: the final command based on the analysis of the parameters +// **err**: it is non-nil if any required property is violated +func ValidateSETParams(checkingForSetAttr bool, wdmp *SetWDMP, override string) (command string, err error){ + for _, sp := range wdmp.Parameters { + if sp.Name == nil { + err = errors.New("name is required for parameters") + return + } + + if checkingForSetAttr { + if sp.Value != nil || sp.Attributes == nil { + checkingForSetAttr = false + } + } else { //in this case, we are just checking valid parameters for SET + if sp.DataType == nil || sp.Value == nil { + err = errors.New("dataType and value are required for SET command") + } + } + } + + if override != "" { + command = override + return + } + + if checkingForSetAttr { // checked for SET_ATTRS properties until the end and found no violation + command = COMMAND_SET_ATTRS + return + } + + command = COMMAND_SET + return +} + +//Validate non-Empty mapping A (nonEmpty keys -> non-Empty(mapping B (string -> string)) +func ValidREPLACEParams(rows map[string]map[string]string) (valid bool){ + for k, v := range rows { + if k == "" || v == nil || len(v) == 0 { + return + } + } + if len(rows) > 0 { + valid = true + } + return +} + + +/* Other helper functions */ +func DecodeJsonPayload(body io.Reader, v interface{}, ReadEntireBody BodyReader) (err error) { + payload, err := ReadEntireBody(body) + + if err != nil { + return + } + + if len(payload) == 0 { + err = ErrJsonEmpty + return + } + + err = json.Unmarshal(payload, v) + return +} + +func GetFromUrlPath(key string, urlVars map[string]string)(val string, exists bool){ + if urlVars!= nil { + val, exists = urlVars[key] + } + return +} diff --git a/src/tr1d1um/conversion_utils_test.go b/src/tr1d1um/conversion_utils_test.go new file mode 100644 index 00000000..f6c77f53 --- /dev/null +++ b/src/tr1d1um/conversion_utils_test.go @@ -0,0 +1,176 @@ +package main + +import ( + "testing" + "encoding/json" + "github.com/stretchr/testify/assert" + "net/http" + "strings" + "io" + "errors" + "net/http/httptest" + "bytes" +) + +var sampleNames = []string{"p1","p2"} + +func TestGetFlavorFormat(t *testing.T) { + assert := assert.New(t) + + t.Run("IdealGet", func (t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://api/device/config?names=p1,p2", nil) + + wdmp := &GetWDMP{Command:COMMAND_GET, Names:sampleNames} + + expected, expectedErr := json.Marshal(wdmp) + actual, actualErr := GetFlavorFormat(req,"attributes", "names", ",") + + assert.EqualValues(expected, actual) + assert.EqualValues(expectedErr, actualErr) + }) + + t.Run("IdealGetAttr", func (t *testing.T){ + req := httptest.NewRequest(http.MethodGet, "http://api/device/config?names=p1,p2&attributes=attr1", + nil) + + wdmp := &GetWDMP{Command:COMMAND_GET_ATTRS, Names:sampleNames, Attribute:"attr1"} + + expected, expectedErr := json.Marshal(wdmp) + actual, actualErr := GetFlavorFormat(req,"attributes", "names", ",") + + assert.EqualValues(expected, actual) + assert.EqualValues(expectedErr, actualErr) + }) + + t.Run("NoNames", func (t *testing.T){ + req := httptest.NewRequest(http.MethodGet, "http://api/device/config?names=", + nil) + + _ , err := GetFlavorFormat(req,"attributes", "names", ",") + + assert.NotNil(err) + assert.True(strings.HasPrefix(err.Error(), "names is a required")) + }) +} + + +func TestAddFlavorFormat(t *testing.T) { + +} + +func TestDeleteFlavorFormat(t *testing.T) { + +} + + +func TestReplaceFlavorFormat(t *testing.T) { + assert := assert.New(t) + cReader := CustomResultReader{} + commonVars := Vars{"uThere?": "yes!"} + emptyVars := Vars{} + + t.Run("TableNotProvided", func (t *testing.T){ + _, err := ReplaceFlavorFormat(nil, emptyVars, "uThere?", cReader.CustomReader) + assert.NotNil(err) + assert.True(strings.HasPrefix(err.Error(), "tableName")) + }) + + t.Run("DecodeJsonErr", func (t *testing.T) { + cReader.data = []byte("") + _, err := ReplaceFlavorFormat(nil, commonVars, "uThere?", cReader.CustomReader) + assert.NotNil(err) + assert.True(strings.HasPrefix(err.Error(), "JSON")) + }) + + t.Run("InvalidParams", func (t *testing.T) { + cReader.data = []byte("{}") + _, err := ReplaceFlavorFormat(nil, commonVars, "uThere?", cReader.CustomReader) + assert.NotNil(err) + assert.True(strings.HasPrefix(err.Error(), "invalid")) + }) + + t.Run("IdealPath", func (t *testing.T) { + wdmpObj := &ReplaceRowsWDMP{Command:COMMAND_REPLACE_ROWS, Table: commonVars["uThere?"]} + wdmpObj.Rows = map[string]map[string]string{"0":{"uno":"one", "dos":"two"}} + cReader.data = []byte(`{"0":{"uno":"one","dos":"two"}}`) + + actual, actualErr := ReplaceFlavorFormat(nil, commonVars, "uThere?", cReader.CustomReader) + + expected, expectedErr := json.Marshal(wdmpObj) + assert.EqualValues(expected, actual) + assert.EqualValues(expectedErr, actualErr) + }) +} + +func TestGetFromUrlPath(t *testing.T) { + assert := assert.New(t) + + fakeUrlVar := map[string]string{"k1":"k1v1,k1v2", "k2": "k2v1"} + + t.Run("NormalCases", func (t *testing.T){ + + k1ValGroup, exists := GetFromUrlPath("k1", fakeUrlVar) + assert.True(exists) + assert.EqualValues("k1v1,k1v2", k1ValGroup) + + k2ValGroup, exists := GetFromUrlPath("k2", fakeUrlVar) + assert.True(exists) + assert.EqualValues("k2v1", k2ValGroup) + }) + + t.Run("NonNilNonExistent", func (t *testing.T){ + _, exists := GetFromUrlPath("k3", fakeUrlVar) + assert.False(exists) + }) + + t.Run("NilCase", func (t *testing.T){ + _, exists := GetFromUrlPath("k", nil) + assert.False(exists) + }) +} + +func TestDecodeJsonPayload(t *testing.T) { + assert := assert.New(t) + + cReader := CustomResultReader{} + + t.Run("ReadEntireBodyFails", func (t *testing.T) { + cReader.err = errors.New("fail") + err := DecodeJsonPayload(nil, nil, cReader.CustomReader) + + assert.NotNil(err) + assert.EqualValues(err.Error(),"fail") + }) + + t.Run("EmptyInput", func (t *testing.T) { + emptyBody := bytes.NewBufferString("") + cReader.data, cReader.err = []byte(""), nil + + err := DecodeJsonPayload(emptyBody, nil, cReader.CustomReader) + + assert.NotNil(err) + assert.EqualValues(err, ErrJsonEmpty) + }) + + t.Run("IdealPath", func (t *testing.T) { + cReader.data = []byte(`{"0":"zero","1":"one"}`) + + expected := map[string]string{"0":"zero", "1":"one"} + actual := make(map[string]string) + + err := DecodeJsonPayload(nil, &actual, cReader.CustomReader) + + assert.Nil(err) + assert.EqualValues(expected, actual) + }) +} + +type CustomResultReader struct { + data []byte + err error +} + +func(c CustomResultReader) CustomReader (_ io.Reader)([]byte,error){ + return c.data, c.err +} + diff --git a/src/tr1d1um/http.go b/src/tr1d1um/http.go new file mode 100644 index 00000000..76f58871 --- /dev/null +++ b/src/tr1d1um/http.go @@ -0,0 +1,125 @@ +package main +import ( + "net/http" + "time" + "bytes" + "github.com/go-kit/kit/log" + "github.com/Comcast/webpa-common/logging" + "io/ioutil" + "github.com/Comcast/webpa-common/wrp" + "encoding/json" + "errors" + "io" + "github.com/gorilla/mux" +) +type ConversionHandler struct { + infoLogger log.Logger + errorLogger log.Logger + timeOut time.Duration + targetUrl string + /*These functions should be set during handler set up */ + GetFlavorFormat func(*http.Request, string, string, string) ([]byte, error) + SetFlavorFormat func(*http.Request, BodyReader) ([]byte, error) + DeleteFlavorFormat func(Vars, string) ([]byte, error) + AddFlavorFormat func(io.Reader, Vars, string, BodyReader) ([]byte, error) + ReplaceFlavorFormat func(io.Reader, Vars, string, BodyReader) ([]byte, error) +} + +func (sh ConversionHandler) ConversionGETHandler(resp http.ResponseWriter, req *http.Request){ + wdmpPayload, err := sh.GetFlavorFormat(req, "attributes", "names", ",") + + if err != nil { + sh.errorLogger.Log(logging.MessageKey(), ERR_UNSUCCESSFUL_DATA_PARSE, logging.ErrorKey(), err.Error()) + return + } + + wrpMessage := &wrp.SimpleRequestResponse{Type:wrp.SimpleRequestResponseMessageType, Payload:wdmpPayload} + + sh.SendData(resp, wrpMessage) +} + +func (sh ConversionHandler) ConversionSETHandler(resp http.ResponseWriter, req *http.Request){ + wdmpPayload, err := sh.SetFlavorFormat(req, ioutil.ReadAll) + + if err != nil { + sh.errorLogger.Log(logging.MessageKey(), ERR_UNSUCCESSFUL_DATA_PARSE, logging.ErrorKey(), err.Error()) + return + } + + wrpMessage := &wrp.SimpleRequestResponse{Type:wrp.SimpleRequestResponseMessageType, Payload:wdmpPayload} + + sh.SendData(resp, wrpMessage) +} + +func (sh ConversionHandler) ConversionDELETEHandler(resp http.ResponseWriter, req *http.Request){ + wdmpPayload, err := sh.DeleteFlavorFormat(mux.Vars(req), "parameter") + + if err != nil { + sh.errorLogger.Log(logging.MessageKey(), ERR_UNSUCCESSFUL_DATA_PARSE, logging.ErrorKey(), err.Error()) + return + } + + wrpMessage := &wrp.SimpleRequestResponse{Type:wrp.SimpleRequestResponseMessageType, Payload:wdmpPayload} + + sh.SendData(resp, wrpMessage) +} + +func (sh ConversionHandler) ConversionREPLACEHandler(resp http.ResponseWriter, req *http.Request){ + wdmpPayload, err := sh.ReplaceFlavorFormat(req.Body, mux.Vars(req), "parameter", ioutil.ReadAll) + + if err != nil { + sh.errorLogger.Log(logging.MessageKey(), ERR_UNSUCCESSFUL_DATA_PARSE, logging.ErrorKey(), err.Error()) + return + } + + wrpMessage := &wrp.SimpleRequestResponse{Type:wrp.SimpleRequestResponseMessageType, Payload:wdmpPayload} + + sh.SendData(resp, wrpMessage) +} + +func (sh ConversionHandler) ConversionADDHandler(resp http.ResponseWriter, req *http.Request){ + wdmpPayload, err := sh.AddFlavorFormat(req.Body, mux.Vars(req), "parameter", ioutil.ReadAll) + + if err != nil { + sh.errorLogger.Log(logging.MessageKey(), ERR_UNSUCCESSFUL_DATA_PARSE, logging.ErrorKey(), err.Error()) + return + } + + wrpMessage := &wrp.SimpleRequestResponse{Type:wrp.SimpleRequestResponseMessageType, Payload:wdmpPayload} + + sh.SendData(resp, wrpMessage) +} + +func (sh ConversionHandler) SendData(resp http.ResponseWriter, wrpMessage *wrp.SimpleRequestResponse){ + wrpPayload, err := json.Marshal(wrpMessage) + + if err != nil { + err = errors.New("unsuccessful wrp conversion to json") + return + } + + clientWithDeadline := http.Client{Timeout:sh.timeOut} + + //todo: any headers to be added here + requestToServer, err := http.NewRequest("GET", sh.targetUrl, bytes.NewBuffer(wrpPayload)) + if err != nil { + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte("Error creating new request")) + sh.errorLogger.Log(logging.MessageKey(), "Could not create new request", logging.ErrorKey(), err.Error()) + return + } + + respFromServer, err := clientWithDeadline.Do(requestToServer) + + if err != nil{ + resp.WriteHeader(http.StatusInternalServerError) + resp.Write([]byte("Error while posting request")) + sh.errorLogger.Log(logging.MessageKey(), "Could not complete request", logging.ErrorKey(), err.Error()) + return + } + + //Try forwarding back the response to the initial requester + resp.WriteHeader(respFromServer.StatusCode) + resp.Write([]byte(respFromServer.Status)) +} + diff --git a/src/tr1d1um/http_test.go b/src/tr1d1um/http_test.go new file mode 100644 index 00000000..a1ad8aec --- /dev/null +++ b/src/tr1d1um/http_test.go @@ -0,0 +1,33 @@ +package main + +type logTracker struct { + keys []interface{} + vals []interface{} +} + +func (fake *logTracker) Log(keyVals ...interface{}) (err error) { + for i, keyVal := range keyVals{ + if i % 2 == 0{ + fake.keys = append(fake.keys, keyVal) + } else { + fake.vals = append(fake.vals, keyVal) + } + } + return +} +/* +func TestConversionGETHandlerWrapFailure(t *testing.T) { + assert := assert.New(t) + conversionHanlder := new(ConversionHandler) + SetupTestingConditions(true, false, conversionHanlder) + req, err := http.NewRequest("GET", "/device/config?names=param1,param2", nil) + if err != nil { + assert.FailNow("Could not make new request") + } + conversionHanlder.ConversionGETHandler(nil, req) + errorMessage := conversionHanlder.errorLogger.(*logTracker).vals[0].(string) + assert.EqualValues(ERR_UNSUCCESSFUL_DATA_WRAP,errorMessage) +} +*/ +//todo: more cases + diff --git a/src/tr1d1um/tr1d1um.go b/src/tr1d1um/tr1d1um.go index 18d5e29a..0c7d95a5 100644 --- a/src/tr1d1um/tr1d1um.go +++ b/src/tr1d1um/tr1d1um.go @@ -1,7 +1,180 @@ package main -import "fmt" +import ( + "os" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/Comcast/webpa-common/server" + "github.com/Comcast/webpa-common/logging" + "github.com/gorilla/mux" + "github.com/Comcast/webpa-common/secure/handler" + "github.com/Comcast/webpa-common/secure" + "github.com/SermoDigital/jose/jwt" + "github.com/justinas/alice" + "github.com/Comcast/webpa-common/secure/key" + "time" + "github.com/go-kit/kit/log" + "net/http" +) + +const ( + applicationName = "tr1d1um" + DefaultKeyId = "current" +) +func tr1d1um(arguments []string) int { + var ( + f= pflag.NewFlagSet(applicationName, pflag.ContinueOnError) + v= viper.New() + logger, _, err= server.Initialize(applicationName, arguments, f, v) + ) + + if err != nil { + logging.Error(logger).Log(logging.MessageKey(), "Unable to initialize Viper environment: %s\n", + logging.ErrorKey(), err) + return 1 + } + + var ( + messageKey = logging.MessageKey() + errorKey = logging.ErrorKey() + infoLogger = logging.Info(logger) + errorLogger = logging.Error(logger) + ) + + infoLogger.Log("configurationFile", v.ConfigFileUsed()) + + tConfig := new(Tr1d1umConfig) + err = v.Unmarshal(tConfig) //todo: decide best way to get current unexported fields from viper + if err != nil { + errorLogger.Log(messageKey,"Unable to unmarshal configuration data into struct", errorKey, err) + return 1 + } + + preHandler, err := SetUpPreHandler(v, logger) + + if err != nil { + infoLogger.Log(messageKey,"Error setting up pre handler", errorKey, err) + return 1 + } + + conversionHandler := SetUpHandler(tConfig, errorLogger, infoLogger) + + r := mux.NewRouter() + + r = AddRoutes(r, preHandler, conversionHandler) + + //todo: finish this initialization method + + return 0 +} + +func AddRoutes(r *mux.Router, preHandler *alice.Chain, conversionHandler *ConversionHandler) (* mux.Router) { + var BodyNonNil = func(request *http.Request, match *mux.RouteMatch) bool { + return request.Body != nil + } + + //todo: inquire about API version + r.Handle("/device/{deviceid}/{service}", preHandler.ThenFunc(conversionHandler.ConversionGETHandler)). + Methods(http.MethodGet) + + r.Handle("/device/{deviceid}/{service}", preHandler.ThenFunc(conversionHandler.ConversionSETHandler)). + Methods(http.MethodPatch).MatcherFunc(BodyNonNil) + + r.Handle("/device/{deviceid}/{service}/{parameter}", preHandler.ThenFunc(conversionHandler. + ConversionDELETEHandler)).Methods(http.MethodDelete) + + r.Handle("/device/{deviceid}/{service}/{parameter}", preHandler.ThenFunc(conversionHandler. + ConversionADDHandler)).Methods(http.MethodPost).MatcherFunc(BodyNonNil) + + r.Handle("/device/{deviceid}/{service}/{parameter}", preHandler.ThenFunc(conversionHandler. + ConversionREPLACEHandler)).Methods(http.MethodPut).MatcherFunc(BodyNonNil) + + return r +} + +// getValidator returns validator for JWT tokens +func GetValidator(v *viper.Viper) (validator secure.Validator, err error) { + default_validators := make(secure.Validators, 0, 0) + var jwtVals []JWTValidator + + v.UnmarshalKey("jwtValidators", &jwtVals) + + // make sure there is at least one jwtValidator supplied + if len(jwtVals) < 1 { + validator = default_validators + return + } + + // if a JWTKeys section was supplied, configure a JWS validator + // and append it to the chain of validators + validators := make(secure.Validators, 0, len(jwtVals)) + + for _, validatorDescriptor := range jwtVals { + var keyResolver key.Resolver + keyResolver, err = validatorDescriptor.Keys.NewResolver() + if err != nil { + validator = validators + return + } + + validators = append( + validators, + secure.JWSValidator{ + DefaultKeyId: DefaultKeyId, + Resolver: keyResolver, + JWTValidators: []*jwt.Validator{validatorDescriptor.Custom.New()}, + }, + ) + } + + // TODO: This should really be part of the unmarshalled validators somehow + basicAuth := v.GetStringSlice("authHeader") + for _, authValue := range basicAuth { + validators = append( + validators, + secure.ExactMatchValidator(authValue), + ) + } + + validator = validators + + return +} + +func SetUpHandler(tConfig *Tr1d1umConfig, errorLogger log.Logger, infoLogger log.Logger)(cHandler *ConversionHandler){ + cHandler = &ConversionHandler{timeOut: time.Duration(tConfig.timeOut), targetUrl: tConfig.targetUrl} + //pass loggers + cHandler.errorLogger = errorLogger + cHandler.infoLogger = infoLogger + //set functions + cHandler.GetFlavorFormat = GetFlavorFormat + cHandler.SetFlavorFormat = SetFlavorFormat + cHandler.DeleteFlavorFormat = DeleteFlavorFormat + cHandler.ReplaceFlavorFormat = ReplaceFlavorFormat + cHandler.AddFlavorFormat = AddFlavorFormat + + return +} + +func SetUpPreHandler(v *viper.Viper, logger log.Logger) (preHandler *alice.Chain, err error){ + validator, err := GetValidator(v) + if err != nil { + return + } + + authHandler := handler.AuthorizationHandler{ + HeaderName: "Authorization", + ForbiddenStatusCode: 403, + Validator: validator, + Logger: logger, + } + + newPreHandler := alice.New(authHandler.Decorate) + preHandler = &newPreHandler + return +} func main() { - fmt.Println("I am tr1d1um.") + os.Exit(tr1d1um(os.Args)) } + diff --git a/src/tr1d1um/tr1d1um_type.go b/src/tr1d1um/tr1d1um_type.go new file mode 100644 index 00000000..2944d694 --- /dev/null +++ b/src/tr1d1um/tr1d1um_type.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/Comcast/webpa-common/secure/key" + "github.com/Comcast/webpa-common/secure" +) + +type Tr1d1umConfig struct { + Addr string `json:"addr"` + PprofAddr string `json:"pprofaddr"` + Cert string `json:"cert"` + Key string `json:"key"` + AuthKey []string `json:"authKey"` + HandlerTimeout string `json:"handlerTimeout"` + HttpTimeout string `json:"httpTimeout"` + HealthInterval string `json:"healthInterval"` + Version string `json:"version"` + MaxApiTcpConns int64 `json:"maxApiTcpConnections"` + MaxHealthTcpConns int64 `json:"maxHealthTcpConnections"` + ServiceList []string `json:"serviceList"` + WrpSource string `json:"wrpSource"` + timeOut int64 `json:"timeOut"` + targetUrl string `json:"targetUrl"` + + JWTValidators []struct { + // JWTKeys is used to create the key.Resolver for JWT verification keys + Keys key.ResolverFactory `json:"keys"` + + // Custom is an optional configuration section that defines + // custom rules for validation over and above the standard RFC rules. + Custom secure.JWTValidatorFactory `json:"custom"` + } `json:"jwtValidators"` +} + +type JWTValidator struct { + // JWTKeys is used to create the key.Resolver for JWT verification keys + Keys key.ResolverFactory + + // Custom is an optional configuration section that defines + // custom rules for validation over and above the standard RFC rules. + Custom secure.JWTValidatorFactory +} +