/* Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package channelparticipation import ( "encoding/json" "io/ioutil" "mime" "mime/multipart" "net/http" "path" "strings" "github.com/golang/protobuf/proto" "github.com/gorilla/mux" cb "github.com/hyperledger/fabric-protos-go/common" "github.com/hyperledger/fabric/common/configtx" "github.com/hyperledger/fabric/common/flogging" "github.com/hyperledger/fabric/orderer/common/localconfig" "github.com/hyperledger/fabric/orderer/common/types" "github.com/pkg/errors" ) const ( URLBaseV1 = "/participation/v1/" URLBaseV1Channels = URLBaseV1 + "channels" FormDataConfigBlockKey = "config-block" channelIDKey = "channelID" urlWithChannelIDKey = URLBaseV1Channels + "/{" + channelIDKey + "}" ) //go:generate counterfeiter -o mocks/channel_management.go -fake-name ChannelManagement . ChannelManagement type ChannelManagement interface { // ChannelList returns a slice of ChannelInfoShort containing all application channels (excluding the system // channel), and ChannelInfoShort of the system channel (nil if does not exist). // The URL fields are empty, and are to be completed by the caller. ChannelList() types.ChannelList // ChannelInfo provides extended status information about a channel. // The URL field is empty, and is to be completed by the caller. ChannelInfo(channelID string) (types.ChannelInfo, error) // JoinChannel instructs the orderer to create a channel and join it with the provided config block. // The URL field is empty, and is to be completed by the caller. JoinChannel(channelID string, configBlock *cb.Block) (types.ChannelInfo, error) // RemoveChannel instructs the orderer to remove a channel. RemoveChannel(channelID string) error } // HTTPHandler handles all the HTTP requests to the channel participation API. type HTTPHandler struct { logger *flogging.FabricLogger config localconfig.ChannelParticipation registrar ChannelManagement router *mux.Router } func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelManagement) *HTTPHandler { handler := &HTTPHandler{ logger: flogging.MustGetLogger("orderer.common.channelparticipation"), config: config, registrar: registrar, router: mux.NewRouter(), } // swagger:operation GET /v1/participation/channels/{channelID} channels listChannel // --- // summary: Returns detailed channel information for a specific channel Ordering Service Node (OSN) has joined. // parameters: // - name: channelID // in: path // description: Channel ID // required: true // type: string // responses: // '200': // description: Successfully retrieved channel. // schema: // "$ref": "#/definitions/channelInfo" // headers: // Content-Type: // description: The media type of the resource // type: string // Cache-Control: // description: The directives for caching responses // type: string handler.router.HandleFunc(urlWithChannelIDKey, handler.serveListOne).Methods(http.MethodGet) // swagger:operation DELETE /v1/participation/channels/{channelID} channels removeChannel // --- // summary: Removes an Ordering Service Node (OSN) from a channel. // parameters: // - name: channelID // in: path // description: Channel ID // required: true // type: string // responses: // '204': // description: Successfully removed channel. // '400': // description: Bad request. // '404': // description: The channel does not exist. // '409': // description: The channel is pending removal. handler.router.HandleFunc(urlWithChannelIDKey, handler.serveRemove).Methods(http.MethodDelete) handler.router.HandleFunc(urlWithChannelIDKey, handler.serveNotAllowed) // swagger:operation GET /v1/participation/channels channels listChannels // --- // summary: Returns the complete list of channels an Ordering Service Node (OSN) has joined. // responses: // '200': // description: Successfully retrieved channels. // schema: // "$ref": "#/definitions/channelList" // headers: // Content-Type: // description: The media type of the resource // type: string // Cache-Control: // description: The directives for caching responses // type: string handler.router.HandleFunc(URLBaseV1Channels, handler.serveListAll).Methods(http.MethodGet) // swagger:operation POST /v1/participation/channels channels joinChannel // --- // summary: Joins an Ordering Service Node (OSN) to a channel. // description: If a channel does not yet exist, it will be created. // parameters: // - name: configBlock // in: formData // type: string // required: true // responses: // '201': // description: Successfully joined channel. // schema: // "$ref": "#/definitions/channelInfo" // headers: // Content-Type: // description: The media type of the resource // type: string // Location: // description: The URL to redirect a page to // type: string // '400': // description: Cannot join channel. // '405': // description: The client is trying to join an app-channel that exists. // '409': // description: The client is trying to join a channel that is currently being removed. // '500': // description: Removal of channel failed. // consumes: // - multipart/form-data handler.router.HandleFunc(URLBaseV1Channels, handler.serveJoin).Methods(http.MethodPost).HeadersRegexp( "Content-Type", "multipart/form-data*") handler.router.HandleFunc(URLBaseV1Channels, handler.serveBadContentType).Methods(http.MethodPost) handler.router.HandleFunc(URLBaseV1Channels, handler.serveNotAllowed) handler.router.HandleFunc(URLBaseV1, handler.redirectBaseV1).Methods(http.MethodGet) return handler } func (h *HTTPHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { if !h.config.Enabled { err := errors.New("channel participation API is disabled") h.sendResponseJsonError(resp, http.StatusServiceUnavailable, err) return } h.router.ServeHTTP(resp, req) } // List all channels func (h *HTTPHandler) serveListAll(resp http.ResponseWriter, req *http.Request) { _, err := negotiateContentType(req) // Only application/json responses for now if err != nil { h.sendResponseJsonError(resp, http.StatusNotAcceptable, err) return } channelList := h.registrar.ChannelList() if channelList.SystemChannel != nil && channelList.SystemChannel.Name != "" { channelList.SystemChannel.URL = path.Join(URLBaseV1Channels, channelList.SystemChannel.Name) } for i, info := range channelList.Channels { channelList.Channels[i].URL = path.Join(URLBaseV1Channels, info.Name) } resp.Header().Set("Cache-Control", "no-store") h.sendResponseOK(resp, channelList) } // List a single channel func (h *HTTPHandler) serveListOne(resp http.ResponseWriter, req *http.Request) { _, err := negotiateContentType(req) // Only application/json responses for now if err != nil { h.sendResponseJsonError(resp, http.StatusNotAcceptable, err) return } channelID, err := h.extractChannelID(req, resp) if err != nil { return } infoFull, err := h.registrar.ChannelInfo(channelID) if err != nil { h.sendResponseJsonError(resp, http.StatusNotFound, err) return } infoFull.URL = path.Join(URLBaseV1Channels, infoFull.Name) resp.Header().Set("Cache-Control", "no-store") h.sendResponseOK(resp, infoFull) } func (h *HTTPHandler) redirectBaseV1(resp http.ResponseWriter, req *http.Request) { http.Redirect(resp, req, URLBaseV1Channels, http.StatusFound) } // Join a channel. // Expect multipart/form-data. func (h *HTTPHandler) serveJoin(resp http.ResponseWriter, req *http.Request) { _, err := negotiateContentType(req) // Only application/json responses for now if err != nil { h.sendResponseJsonError(resp, http.StatusNotAcceptable, err) return } _, params, err := mime.ParseMediaType(req.Header.Get("Content-Type")) if err != nil { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Wrap(err, "cannot parse Mime media type")) return } block := h.multipartFormDataBodyToBlock(params, req, resp) if block == nil { return } channelID, err := ValidateJoinBlock(block) if err != nil { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.WithMessage(err, "invalid join block")) return } info, err := h.registrar.JoinChannel(channelID, block) if err != nil { h.sendJoinError(err, resp) return } info.URL = path.Join(URLBaseV1Channels, info.Name) h.logger.Debugf("Successfully joined channel: %s", info.URL) h.sendResponseCreated(resp, info.URL, info) } // Expect a multipart/form-data with a single part, of type file, with key FormDataConfigBlockKey. func (h *HTTPHandler) multipartFormDataBodyToBlock(params map[string]string, req *http.Request, resp http.ResponseWriter) *cb.Block { boundary := params["boundary"] reader := multipart.NewReader( http.MaxBytesReader(resp, req.Body, int64(h.config.MaxRequestBodySize)), boundary, ) form, err := reader.ReadForm(2 * int64(h.config.MaxRequestBodySize)) if err != nil { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Wrap(err, "cannot read form from request body")) return nil } if _, exist := form.File[FormDataConfigBlockKey]; !exist { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Errorf("form does not contains part key: %s", FormDataConfigBlockKey)) return nil } if len(form.File) != 1 || len(form.Value) != 0 { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.New("form contains too many parts")) return nil } fileHeader := form.File[FormDataConfigBlockKey][0] file, err := fileHeader.Open() if err != nil { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Wrapf(err, "cannot open file part %s from request body", FormDataConfigBlockKey)) return nil } blockBytes, err := ioutil.ReadAll(file) if err != nil { h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Wrapf(err, "cannot read file part %s from request body", FormDataConfigBlockKey)) return nil } block := &cb.Block{} err = proto.Unmarshal(blockBytes, block) if err != nil { h.logger.Debugf("Failed to unmarshal blockBytes: %s", err) h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Wrapf(err, "cannot unmarshal file part %s into a block", FormDataConfigBlockKey)) return nil } return block } func (h *HTTPHandler) extractChannelID(req *http.Request, resp http.ResponseWriter) (string, error) { channelID, ok := mux.Vars(req)[channelIDKey] if !ok { err := errors.New("missing channel ID") h.sendResponseJsonError(resp, http.StatusInternalServerError, err) return "", err } if err := configtx.ValidateChannelID(channelID); err != nil { err = errors.WithMessage(err, "invalid channel ID") h.sendResponseJsonError(resp, http.StatusBadRequest, err) return "", err } return channelID, nil } func (h *HTTPHandler) sendJoinError(err error, resp http.ResponseWriter) { h.logger.Debugf("Failed to JoinChannel: %s", err) switch err { case types.ErrSystemChannelExists: // The client is trying to join an app-channel, but the system channel exists: only GET is allowed on app channels. h.sendResponseNotAllowed(resp, errors.WithMessage(err, "cannot join"), http.MethodGet) case types.ErrChannelAlreadyExists: // The client is trying to join an app-channel that exists, but the system channel does not; // The client is trying to join the system-channel, and it exists. GET & DELETE are allowed on the channel. h.sendResponseNotAllowed(resp, errors.WithMessage(err, "cannot join"), http.MethodGet, http.MethodDelete) case types.ErrAppChannelsAlreadyExists: // The client is trying to join the system-channel that does not exist, but app channels exist. h.sendResponseJsonError(resp, http.StatusForbidden, errors.WithMessage(err, "cannot join")) case types.ErrChannelPendingRemoval: // The client is trying to join a channel that is currently being removed. h.sendResponseJsonError(resp, http.StatusConflict, errors.WithMessage(err, "cannot join")) case types.ErrChannelRemovalFailure: h.sendResponseJsonError(resp, http.StatusInternalServerError, errors.WithMessage(err, "cannot join")) default: h.sendResponseJsonError(resp, http.StatusBadRequest, errors.WithMessage(err, "cannot join")) } } // Remove a channel func (h *HTTPHandler) serveRemove(resp http.ResponseWriter, req *http.Request) { _, err := negotiateContentType(req) // Only application/json responses for now if err != nil { h.sendResponseJsonError(resp, http.StatusNotAcceptable, err) return } channelID, err := h.extractChannelID(req, resp) if err != nil { return } err = h.registrar.RemoveChannel(channelID) if err == nil { h.logger.Debugf("Successfully removed channel: %s", channelID) resp.WriteHeader(http.StatusNoContent) return } h.logger.Debugf("Failed to remove channel: %s, err: %s", channelID, err) switch err { case types.ErrSystemChannelExists: h.sendResponseNotAllowed(resp, errors.WithMessage(err, "cannot remove"), http.MethodGet) case types.ErrChannelNotExist: h.sendResponseJsonError(resp, http.StatusNotFound, errors.WithMessage(err, "cannot remove")) case types.ErrChannelPendingRemoval: h.sendResponseJsonError(resp, http.StatusConflict, errors.WithMessage(err, "cannot remove")) default: h.sendResponseJsonError(resp, http.StatusBadRequest, errors.WithMessage(err, "cannot remove")) } } func (h *HTTPHandler) serveBadContentType(resp http.ResponseWriter, req *http.Request) { err := errors.Errorf("unsupported Content-Type: %s", req.Header.Values("Content-Type")) h.sendResponseJsonError(resp, http.StatusBadRequest, err) } func (h *HTTPHandler) serveNotAllowed(resp http.ResponseWriter, req *http.Request) { err := errors.Errorf("invalid request method: %s", req.Method) if _, ok := mux.Vars(req)[channelIDKey]; ok { h.sendResponseNotAllowed(resp, err, http.MethodGet, http.MethodDelete) return } h.sendResponseNotAllowed(resp, err, http.MethodGet, http.MethodPost) } func negotiateContentType(req *http.Request) (string, error) { acceptReq := req.Header.Get("Accept") if len(acceptReq) == 0 { return "application/json", nil } options := strings.Split(acceptReq, ",") for _, opt := range options { if strings.Contains(opt, "application/json") || strings.Contains(opt, "application/*") || strings.Contains(opt, "*/*") { return "application/json", nil } } return "", errors.New("response Content-Type is application/json only") } func (h *HTTPHandler) sendResponseJsonError(resp http.ResponseWriter, code int, err error) { encoder := json.NewEncoder(resp) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(code) if err := encoder.Encode(&types.ErrorResponse{Error: err.Error()}); err != nil { h.logger.Errorf("failed to encode error, err: %s", err) } } func (h *HTTPHandler) sendResponseOK(resp http.ResponseWriter, content interface{}) { encoder := json.NewEncoder(resp) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(http.StatusOK) if err := encoder.Encode(content); err != nil { h.logger.Errorf("failed to encode content, err: %s", err) } } func (h *HTTPHandler) sendResponseCreated(resp http.ResponseWriter, location string, content interface{}) { encoder := json.NewEncoder(resp) resp.Header().Set("Location", location) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(http.StatusCreated) if err := encoder.Encode(content); err != nil { h.logger.Errorf("failed to encode content, err: %s", err) } } func (h *HTTPHandler) sendResponseNotAllowed(resp http.ResponseWriter, err error, allow ...string) { encoder := json.NewEncoder(resp) allowVal := strings.Join(allow, ", ") resp.Header().Set("Allow", allowVal) resp.Header().Set("Content-Type", "application/json") resp.WriteHeader(http.StatusMethodNotAllowed) if err := encoder.Encode(&types.ErrorResponse{Error: err.Error()}); err != nil { h.logger.Errorf("failed to encode error, err: %s", err) } }