go_study/fabric-main/orderer/common/channelparticipation/restapi.go

465 lines
16 KiB
Go

/*
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)
}
}