This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new e533f19 feat: consumer api && ssl api enhance (#281)
e533f19 is described below
commit e533f19d6c30753ae83e6968f921129264bcc0a8
Author: nic-chen <[email protected]>
AuthorDate: Sun Jun 28 12:41:00 2020 +0800
feat: consumer api && ssl api enhance (#281)
---
api/errno/error.go | 52 ++++++--
api/route/consumer.go | 148 +++++++++++++++++++++++
api/route/ssl.go | 139 +++++++++++++++-------
api/script/db/schema.sql | 13 ++
api/service/base_test.go | 25 ++++
api/service/consumer.go | 272 ++++++++++++++++++++++++++++++++++++++++++
api/service/consumer_test.go | 134 +++++++++++++++++++++
api/service/ssl.go | 175 +++++++++++++++++++++------
api/service/ssl_test.go | 277 +++++++++++++++++++++++++++++++++++++++++++
9 files changed, 1150 insertions(+), 85 deletions(-)
diff --git a/api/errno/error.go b/api/errno/error.go
index 2247541..2c3917d 100644
--- a/api/errno/error.go
+++ b/api/errno/error.go
@@ -32,6 +32,11 @@ var (
SystemError = Message{"010001", "system error"}
BadRequestError = Message{Code: "010002", Msg: "Request format error"}
NotFoundError = Message{Code: "010003", Msg: "No resources found"}
+ InvalidParam = Message{"010004", "Request parameter error"}
+ DBWriteError = Message{"010005", "Database save failed"}
+ DBReadError = Message{"010006", "Database query failed"}
+ DBDeleteError = Message{"010007", "Database delete failed"}
+ RecordNotExist = Message{"010009", "Record does not exist"}
//BB 01 config module
ConfEnvError = Message{"010101", "Environment variable not found:
%s"}
@@ -50,14 +55,25 @@ var (
ApisixPluginListError = Message{"010301", "List APISIX plugins
failed: %s"}
ApisixPluginSchemaError = Message{"010301", "Find APISIX plugin schema
failed: %s"}
+ // 04 ssl模块
+ SslParseError = Message{"010401", "Certificate resolution
failed: %s"}
+ ApisixSslCreateError = Message{"010402", "Create APISIX SSL failed"}
+ ApisixSslUpdateError = Message{"010403", "Update APISIX SSL failed"}
+ ApisixSslDeleteError = Message{"010404", "Delete APISIX SSL failed"}
+
// 06 upstream
- UpstreamRequestError = Message{"010601", "upstream request parameters
are abnormal: %s"}
- UpstreamTransError = Message{"010602", "upstream parameter conversion
is abnormal: %s"}
- DBUpstreamError = Message{"010603", "upstream storage failure: %s"}
- ApisixUpstreamCreateError = Message{"010604", "apisix upstream
create failure: %s"}
- ApisixUpstreamUpdateError = Message{"010605", "apisix upstream
update failure: %s"}
- ApisixUpstreamDeleteError = Message{"010606", "apisix upstream
delete failure: %s"}
- DBUpstreamDeleteError = Message{"010607", "upstream delete
failure: %s"}
+ UpstreamRequestError = Message{"010601", "upstream request
parameters are abnormal: %s"}
+ UpstreamTransError = Message{"010602", "upstream parameter
conversion is abnormal: %s"}
+ DBUpstreamError = Message{"010603", "upstream storage
failure: %s"}
+ ApisixUpstreamCreateError = Message{"010604", "apisix upstream create
failure: %s"}
+ ApisixUpstreamUpdateError = Message{"010605", "apisix upstream update
failure: %s"}
+ ApisixUpstreamDeleteError = Message{"010606", "apisix upstream delete
failure: %s"}
+ DBUpstreamDeleteError = Message{"010607", "upstream delete failure:
%s"}
+
+ ApisixConsumerCreateError = Message{"010702", "Create APISIX Consumer
failed"}
+ ApisixConsumerUpdateError = Message{"010703", "Update APISIX Consumer
failed"}
+ ApisixConsumerDeleteError = Message{"010704", "Delete APISIX Consumer
failed"}
+ DuplicateUserName = Message{"010705", "Duplicate username"}
)
type ManagerError struct {
@@ -65,6 +81,7 @@ type ManagerError struct {
Code string
Msg string
Data interface{}
+ Detail string
}
// toString
@@ -72,10 +89,18 @@ func (e *ManagerError) Error() string {
return e.Msg
}
+func (e *ManagerError) ErrorDetail() string {
+ return fmt.Sprintf("TraceId: %s, Code: %s, Msg: %s, Detail: %s",
e.TraceId, e.Code, e.Msg, e.Detail)
+}
+
func FromMessage(m Message, args ...interface{}) *ManagerError {
return &ManagerError{TraceId: "", Code: m.Code, Msg: fmt.Sprintf(m.Msg,
args...)}
}
+func New(m Message, args ...interface{}) *ManagerError {
+ return &ManagerError{TraceId: "", Code: m.Code, Msg: m.Msg, Detail:
fmt.Sprintf("%s", args...)}
+}
+
func (e *ManagerError) Response() map[string]interface{} {
return map[string]interface{}{
"code": e.Code,
@@ -105,3 +130,16 @@ func Success() []byte {
result, _ := json.Marshal(w)
return result
}
+
+func Succeed() map[string]interface{} {
+ return FromMessage(SystemSuccess).Response()
+}
+
+type HttpError struct {
+ Code int
+ Msg string
+}
+
+func (e *HttpError) Error() string {
+ return e.Msg
+}
diff --git a/api/route/consumer.go b/api/route/consumer.go
new file mode 100644
index 0000000..ae42b48
--- /dev/null
+++ b/api/route/consumer.go
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package route
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/satori/go.uuid"
+
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/service"
+)
+
+func AppendConsumer(r *gin.Engine) *gin.Engine {
+
+ r.GET("/apisix/admin/consumers", consumerList)
+ r.POST("/apisix/admin/consumers", consumerCreate)
+ r.GET("/apisix/admin/consumers/:id", consumerItem)
+ r.PUT("/apisix/admin/consumers/:id", consumerUpdate)
+ r.DELETE("/apisix/admin/consumers/:id", consumerDelete)
+
+ return r
+}
+
+func consumerList(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+
+ search := c.DefaultQuery("search", "")
+
+ count, list, err := service.ConsumerList(page, size, search)
+
+ if err != nil {
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ resp := errno.FromMessage(errno.SystemSuccess).ListResponse(count, list)
+
+ c.JSON(http.StatusOK, resp)
+}
+
+func consumerItem(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ id := c.Param("id")
+
+ consumer, err := service.ConsumerItem(id)
+
+ if err != nil {
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ c.JSON(http.StatusOK,
errno.FromMessage(errno.SystemSuccess).ItemResponse(consumer))
+}
+
+func consumerCreate(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ param, exist := c.Get("requestBody")
+
+ u4 := uuid.NewV4()
+
+ if !exist || len(param.([]byte)) < 1 {
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId,
requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
+ return
+ }
+
+ if err := service.ConsumerCreate(param, u4.String()); err != nil {
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ c.JSON(http.StatusOK, errno.Succeed())
+}
+
+func consumerUpdate(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ param, exist := c.Get("requestBody")
+
+ id := c.Param("id")
+
+ if !exist || len(param.([]byte)) < 1 {
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId,
requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
+ return
+ }
+
+ if err := service.ConsumerUpdate(param, id); err != nil {
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ c.JSON(http.StatusOK, errno.Succeed())
+}
+
+func consumerDelete(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ id := c.Param("id")
+
+ if err := service.ConsumerDelete(id); err != nil {
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ c.JSON(http.StatusOK, errno.Succeed())
+}
diff --git a/api/route/ssl.go b/api/route/ssl.go
index 2a9d6f7..cc548c5 100644
--- a/api/route/ssl.go
+++ b/api/route/ssl.go
@@ -23,131 +23,182 @@ import (
"github.com/gin-gonic/gin"
"github.com/satori/go.uuid"
+ "github.com/apisix/manager-api/conf"
"github.com/apisix/manager-api/errno"
"github.com/apisix/manager-api/service"
)
-const contentType = "application/json"
-
func AppendSsl(r *gin.Engine) *gin.Engine {
r.POST("/apisix/admin/check_ssl_cert", sslCheck)
+
r.GET("/apisix/admin/ssls", sslList)
r.POST("/apisix/admin/ssls", sslCreate)
r.GET("/apisix/admin/ssls/:id", sslItem)
r.PUT("/apisix/admin/ssls/:id", sslUpdate)
r.DELETE("/apisix/admin/ssls/:id", sslDelete)
+ r.PATCH("/apisix/admin/ssls/:id", sslPatch)
+
return r
}
func sslList(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
- // todo params check
- resp, err := service.SslList(page, size)
+ status, _ := strconv.Atoi(c.DefaultQuery("status", "-1"))
+ expireStart, _ := strconv.Atoi(c.DefaultQuery("expire_start", "-1"))
+ expireEnd, _ := strconv.Atoi(c.DefaultQuery("expire_end", "-1"))
+
+ sni := c.DefaultQuery("sni", "")
+
+ count, list, err := service.SslList(page, size, status, expireStart,
expireEnd, sni)
if err != nil {
- e := errno.FromMessage(errno.RouteRequestError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
return
}
- c.Data(http.StatusOK, service.ContentType, resp)
+ resp := errno.FromMessage(errno.SystemSuccess).ListResponse(count, list)
+
+ c.JSON(http.StatusOK, resp)
}
func sslItem(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
id := c.Param("id")
- // todo params check
- resp, err := service.SslItem(id)
+ ssl, err := service.SslItem(id)
if err != nil {
- e := errno.FromMessage(errno.RouteRequestError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
return
}
- c.Data(http.StatusOK, service.ContentType, resp)
+ c.JSON(http.StatusOK,
errno.FromMessage(errno.SystemSuccess).ItemResponse(ssl))
}
func sslCheck(c *gin.Context) {
- // todo params check
+ requestId, _ := c.Get("X-Request-Id")
param, exist := c.Get("requestBody")
if !exist || len(param.([]byte)) < 1 {
- e := errno.FromMessage(errno.RouteRequestError, "route create
with no post data")
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId,
requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
return
}
- resp, err := service.SslCheck(param)
+ ssl, err := service.SslCheck(param)
if err != nil {
- e := errno.FromMessage(errno.RouteRequestError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
return
}
- c.Data(http.StatusOK, contentType, resp)
+ resp := errno.FromMessage(errno.SystemSuccess).ItemResponse(ssl)
+
+ c.JSON(http.StatusOK, resp)
}
func sslCreate(c *gin.Context) {
- // todo params check
+ requestId, _ := c.Get("X-Request-Id")
param, exist := c.Get("requestBody")
u4 := uuid.NewV4()
if !exist || len(param.([]byte)) < 1 {
- e := errno.FromMessage(errno.RouteRequestError, "route create
with no post data")
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId,
requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
return
}
if err := service.SslCreate(param, u4.String()); err != nil {
- e := errno.FromMessage(errno.ApisixRouteCreateError,
err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError,
e.Response())
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
return
}
- c.Data(http.StatusOK, contentType, errno.Success())
+ c.JSON(http.StatusOK, errno.Succeed())
}
func sslUpdate(c *gin.Context) {
- // todo params check
+ requestId, _ := c.Get("X-Request-Id")
param, exist := c.Get("requestBody")
id := c.Param("id")
if !exist || len(param.([]byte)) < 1 {
- e := errno.FromMessage(errno.RouteRequestError, "route create
with no post data")
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId,
requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
return
}
if err := service.SslUpdate(param, id); err != nil {
- e := errno.FromMessage(errno.ApisixRouteCreateError,
err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusInternalServerError,
e.Response())
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ c.JSON(http.StatusOK, errno.Succeed())
+}
+
+func sslPatch(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
+ param, exist := c.Get("requestBody")
+
+ id := c.Param("id")
+
+ if !exist || len(param.([]byte)) < 1 {
+ err := errno.New(errno.InvalidParam)
+ logger.WithField(conf.RequestId,
requestId).Error(err.ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusBadRequest, err.Response())
return
}
- c.Data(http.StatusOK, contentType, errno.Success())
+ if err := service.SslPatch(param, id); err != nil {
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
+ return
+ }
+
+ c.JSON(http.StatusOK, errno.Succeed())
}
func sslDelete(c *gin.Context) {
+ requestId, _ := c.Get("X-Request-Id")
id := c.Param("id")
- // todo params check
+
if err := service.SslDelete(id); err != nil {
- e := errno.FromMessage(errno.RouteRequestError, err.Error())
- logger.Error(e.Msg)
- c.AbortWithStatusJSON(http.StatusBadRequest, e.Response())
+ if httpError, ok := err.(*errno.HttpError); ok {
+ logger.WithField(conf.RequestId, requestId).Error(err)
+ c.AbortWithStatusJSON(httpError.Code, httpError.Msg)
+ return
+ }
+
+ logger.WithField(conf.RequestId,
requestId).Error(err.(*errno.ManagerError).ErrorDetail())
+ c.AbortWithStatusJSON(http.StatusInternalServerError,
err.(*errno.ManagerError).Response())
return
}
- c.Data(http.StatusOK, service.ContentType, errno.Success())
+ c.JSON(http.StatusOK, errno.Succeed())
}
diff --git a/api/script/db/schema.sql b/api/script/db/schema.sql
index 300315f..83c4bdd 100644
--- a/api/script/db/schema.sql
+++ b/api/script/db/schema.sql
@@ -18,6 +18,7 @@ CREATE TABLE `routes` (
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
+
CREATE TABLE `ssls` (
`id` char(36) NOT NULL DEFAULT '',
`public_key` text NOT NULL,
@@ -29,6 +30,7 @@ CREATE TABLE `ssls` (
`update_time` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
+
-- upstream
CREATE TABLE `upstreams` (
`id` varchar(64) NOT NULL unique,
@@ -40,4 +42,15 @@ CREATE TABLE `upstreams` (
`create_time` bigint(20),
`update_time` bigint(20),
PRIMARY KEY (`id`)
+) DEFAULT CHARSET=utf8;
+
+CREATE TABLE `consumers` (
+ `id` char(36) NOT NULL DEFAULT '',
+ `username` varchar(100) DEFAULT '',
+ `plugins` text,
+ `desc` varchar(200) DEFAULT '',
+ `create_time` int(10) unsigned NOT NULL,
+ `update_time` int(10) unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uni_username` (`username`)
) DEFAULT CHARSET=utf8;
\ No newline at end of file
diff --git a/api/service/base_test.go b/api/service/base_test.go
new file mode 100644
index 0000000..dc665a9
--- /dev/null
+++ b/api/service/base_test.go
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "github.com/apisix/manager-api/conf"
+)
+
+func init() {
+ conf.InitializeMysql()
+}
diff --git a/api/service/consumer.go b/api/service/consumer.go
new file mode 100644
index 0000000..1af3772
--- /dev/null
+++ b/api/service/consumer.go
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/satori/go.uuid"
+
+ "github.com/apisix/manager-api/conf"
+ "github.com/apisix/manager-api/errno"
+ "github.com/apisix/manager-api/utils"
+)
+
+type Consumer struct {
+ Base
+ Username string `json:"username"`
+ Desc string `json:"desc"`
+ Plugins string `json:"plugins"`
+}
+
+type ConsumerDto struct {
+ Base
+ Username string `json:"username"`
+ Desc string `json:"desc"`
+ Plugins map[string]interface{} `json:"plugins"`
+}
+
+type ApisixConsumer struct {
+ Username string `json:"username"`
+ Desc string `json:"desc"`
+ Plugins map[string]interface{} `json:"plugins"`
+}
+
+func (apisixConsumer *ApisixConsumer) Transfer(consumer *ConsumerDto) error {
+ apisixConsumer.Username = consumer.Username
+ apisixConsumer.Desc = consumer.Desc
+ apisixConsumer.Plugins = consumer.Plugins
+
+ return nil
+}
+
+func (consumer *Consumer) Transfer(req *ConsumerDto) error {
+ consumer.ID = req.ID
+ consumer.Desc = req.Desc
+ consumer.Username = req.Username
+
+ plugins, _ := json.Marshal(req.Plugins)
+ consumer.Plugins = string(plugins)
+
+ return nil
+}
+
+func (dto *ConsumerDto) Transfer(consumer *Consumer) error {
+ dto.ID = consumer.ID
+ dto.Desc = consumer.Desc
+ dto.Username = consumer.Username
+ dto.CreateTime = consumer.CreateTime
+ dto.UpdateTime = consumer.UpdateTime
+
+ var plugins map[string]interface{}
+ _ = json.Unmarshal([]byte(consumer.Plugins), &plugins)
+ dto.Plugins = plugins
+
+ return nil
+}
+
+// ApisixConsumerResponse is response from apisix admin api
+type ApisixConsumerResponse struct {
+ Action string `json:"action"`
+ Node *ConsumerNode `json:"node"`
+}
+
+type ConsumerNode struct {
+ Value ApisixConsumer `json:"value"`
+ ModifiedIndex uint64 `json:"modifiedIndex"`
+}
+
+func (req *ConsumerDto) Parse(body interface{}) {
+ if err := json.Unmarshal(body.([]byte), req); err != nil {
+ req = nil
+ }
+}
+
+func ConsumerList(page, size int, search string) (int, []ConsumerDto, error) {
+ var count int
+ consumerList := []Consumer{}
+ db := conf.DB().Table("consumers")
+
+ if search != "" {
+ db = db.Where("name like ? ", "%"+search+"%").
+ Or("description like ? ", "%"+search+"%")
+ }
+
+ if err := db.Order("create_time desc").Offset((page - 1) *
size).Limit(size).Find(&consumerList).Error; err != nil {
+ e := errno.New(errno.DBReadError, err.Error())
+ return 0, nil, e
+ }
+ if err := db.Count(&count).Error; err != nil {
+ e := errno.New(errno.DBReadError, err.Error())
+ return 0, nil, e
+ }
+
+ dtoList := []ConsumerDto{}
+
+ for _, consumer := range consumerList {
+ dto := ConsumerDto{}
+ dto.Transfer(&consumer)
+
+ dtoList = append(dtoList, dto)
+ }
+
+ return count, dtoList, nil
+}
+
+func ConsumerItem(id string) (*ConsumerDto, error) {
+ consumer := &Consumer{}
+ if err := conf.DB().Table("consumers").Where("id = ?",
id).First(consumer).Error; err != nil {
+ e := errno.New(errno.DBReadError, err.Error())
+ return nil, e
+ }
+
+ dto := &ConsumerDto{}
+ dto.Transfer(consumer)
+
+ return dto, nil
+}
+
+func ConsumerCreate(param interface{}, id string) error {
+ req := &ConsumerDto{}
+ req.Parse(param)
+
+ exists := Consumer{}
+ conf.DB().Table("consumers").Where("username = ?",
req.Username).First(&exists)
+ if exists != (Consumer{}) {
+ e := errno.New(errno.DuplicateUserName)
+ return e
+ }
+
+ apisixConsumer := &ApisixConsumer{}
+ apisixConsumer.Transfer(req)
+
+ if _, err := apisixConsumer.PutConsumerToApisix(req.Username); err !=
nil {
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixSslCreateError, err.Error())
+ return e
+ }
+
+ consumer := &Consumer{}
+ consumer.Transfer(req)
+
+ // update mysql
+ consumer.ID = uuid.FromStringOrNil(id)
+ if err := conf.DB().Create(consumer).Error; err != nil {
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
+ return nil
+}
+
+func ConsumerUpdate(param interface{}, id string) error {
+ req := &ConsumerDto{}
+ req.Parse(param)
+
+ exists := Consumer{}
+ conf.DB().Table("consumers").Where("username = ?",
req.Username).First(&exists)
+ if exists != (Consumer{}) && exists.ID != req.ID {
+ e := errno.New(errno.DuplicateUserName)
+ return e
+ }
+
+ apisixConsumer := &ApisixConsumer{}
+ apisixConsumer.Transfer(req)
+
+ if _, err := apisixConsumer.PutConsumerToApisix(req.Username); err !=
nil {
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixConsumerUpdateError, err.Error())
+ return e
+ }
+
+ // update mysql
+ consumer := &Consumer{}
+ consumer.Transfer(req)
+ consumer.ID = uuid.FromStringOrNil(id)
+ if err := conf.DB().Model(&consumer).Updates(consumer).Error; err !=
nil {
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
+ return nil
+}
+
+func ConsumerDelete(id string) error {
+ //
+ consumer := &Consumer{}
+ if err := conf.DB().Table("consumers").Where("id = ?",
id).First(consumer).Error; err != nil {
+ e := errno.New(errno.RecordNotExist, err.Error())
+ return e
+ }
+
+ if _, err := consumer.DeleteConsumerFromApisix(); err != nil {
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixConsumerDeleteError, err.Error())
+ return e
+ }
+ // delete from mysql
+ if err := conf.DB().Delete(consumer).Error; err != nil {
+ return errno.New(errno.DBDeleteError, err.Error())
+ }
+
+ return nil
+}
+
+func (req *ApisixConsumer) PutConsumerToApisix(rid string)
(*ApisixConsumerResponse, error) {
+ url := fmt.Sprintf("%s/consumers/%s", conf.BaseUrl, rid)
+ if data, err := json.Marshal(req); err != nil {
+ return nil, err
+ } else {
+ if resp, err := utils.Put(url, data); err != nil {
+ logger.Error(url)
+ logger.Error(string(data))
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixConsumerResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error(), resp)
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+ }
+}
+
+func (req *Consumer) DeleteConsumerFromApisix() (*ApisixConsumerResponse,
error) {
+ id := req.Username
+ url := fmt.Sprintf("%s/consumers/%s", conf.BaseUrl, id)
+
+ if resp, err := utils.Delete(url); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixConsumerResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+}
diff --git a/api/service/consumer_test.go b/api/service/consumer_test.go
new file mode 100644
index 0000000..2d47fde
--- /dev/null
+++ b/api/service/consumer_test.go
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "testing"
+
+ "github.com/satori/go.uuid"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/apisix/manager-api/errno"
+)
+
+var (
+ c1 = uuid.NewV4()
+ c2 = uuid.NewV4()
+)
+
+func TestConsumerCurd(t *testing.T) {
+ //test3.com
+ param := []byte(`{
+ "username": "test_consumer",
+ "plugins": {
+ "key-auth": {
+ "key": "auth-one"
+ },
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ },
+ "desc": "test description"
+}`)
+
+ err := ConsumerCreate(param, c1.String())
+
+ assert := assert.New(t)
+ assert.Nil(err)
+
+ //get item
+ consumer, err := ConsumerItem(c1.String())
+ assert.Nil(err)
+ assert.Equal("test_consumer", consumer.Username)
+ assert.Equal(2, len(consumer.Plugins))
+
+ //duplicate username fail
+ param = []byte(`{
+ "username": "test_consumer",
+ "plugins": {
+ "key-auth": {
+ "key": "auth-one"
+ },
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ },
+ "desc": "test description"
+}`)
+
+ err = ConsumerCreate(param, c2.String())
+ assert.NotNil(err)
+ assert.Equal(errno.DuplicateUserName.Code,
err.(*errno.ManagerError).Code)
+
+ // ok
+ param = []byte(`{
+ "username": "test_consumer2",
+ "plugins": {
+ "key-auth": {
+ "key": "auth-one"
+ }
+ },
+ "desc": "test description2"
+}`)
+
+ err = ConsumerCreate(param, c2.String())
+ assert.Nil(err)
+
+ //list
+ count, list, err := ConsumerList(2, 1, "")
+ assert.Equal(true, count >= 2)
+ assert.Equal(1, len(list))
+
+ //update
+ param = []byte(`{
+ "username": "test_consumer1",
+ "plugins": {
+ "key-auth": {
+ "key": "auth-one"
+ },
+ "limit-count": {
+ "count": 2,
+ "time_window": 60,
+ "rejected_code": 503,
+ "key": "remote_addr"
+ }
+ },
+ "desc": "test description"
+}`)
+
+ err = ConsumerUpdate(param, c1.String())
+ assert.Nil(err)
+
+ consumer, _ = ConsumerItem(c1.String())
+ assert.Equal("test_consumer1", consumer.Username)
+
+ //delete
+ err = ConsumerDelete(c1.String())
+ assert.Nil(err)
+
+ count2, _, err := ConsumerList(2, 1, "")
+ assert.Equal(count2, count-1)
+
+ err = ConsumerDelete(c2.String())
+ assert.Nil(err)
+}
diff --git a/api/service/ssl.go b/api/service/ssl.go
index 839e3b8..1960e30 100644
--- a/api/service/ssl.go
+++ b/api/service/ssl.go
@@ -51,9 +51,10 @@ type SslDto struct {
type SslRequest struct {
ID string `json:"id,omitempty"`
- PublicKey string `json:"cert"`
- PrivateKey string `json:"key"`
- Snis []string `json:"snis"`
+ PublicKey string `json:"cert,omitempty"`
+ PrivateKey string `json:"key,omitempty"`
+ Snis []string `json:"snis,omitempty"`
+ Status uint64 `json:"status,omitempty"`
}
// ApisixSslResponse is response from apisix admin api
@@ -91,11 +92,31 @@ func (sslDto *SslDto) Parse(ssl *Ssl) error {
return nil
}
-func SslList(page, size int) ([]byte, error) {
+func SslList(page, size, status, expireStart, expireEnd int, sni string) (int,
[]SslDto, error) {
var count int
sslList := []Ssl{}
- if err := conf.DB().Table("ssls").Offset((page - 1) *
size).Limit(size).Find(&sslList).Count(&count).Error; err != nil {
- return nil, err
+ db := conf.DB().Table("ssls")
+
+ if sni != "" {
+ db = db.Where("snis like ? ", "%"+sni+"%")
+ }
+ if status > -1 {
+ db = db.Where("status = ? ", status)
+ }
+ if expireStart > 0 {
+ db = db.Where("validity_end >= ? ", expireStart)
+ }
+ if expireEnd > 0 {
+ db = db.Where("validity_end <= ? ", expireEnd)
+ }
+
+ if err := db.Order("validity_end desc").Offset((page - 1) *
size).Limit(size).Find(&sslList).Error; err != nil {
+ e := errno.New(errno.DBReadError, err.Error())
+ return 0, nil, e
+ }
+ if err := db.Count(&count).Error; err != nil {
+ e := errno.New(errno.DBReadError, err.Error())
+ return 0, nil, e
}
sslDtoList := []SslDto{}
@@ -107,33 +128,31 @@ func SslList(page, size int) ([]byte, error) {
sslDtoList = append(sslDtoList, sslDto)
}
- data := errno.FromMessage(errno.SystemSuccess).ListResponse(count,
sslDtoList)
-
- return json.Marshal(data)
+ return count, sslDtoList, nil
}
-func SslItem(id string) ([]byte, error) {
+func SslItem(id string) (*SslDto, error) {
ssl := &Ssl{}
if err := conf.DB().Table("ssls").Where("id = ?", id).First(ssl).Error;
err != nil {
- return nil, err
+ e := errno.New(errno.DBReadError, err.Error())
+ return nil, e
}
sslDto := &SslDto{}
sslDto.Parse(ssl)
- data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
-
- return json.Marshal(data)
+ return sslDto, nil
}
-func SslCheck(param interface{}) ([]byte, error) {
+func SslCheck(param interface{}) (*SslDto, error) {
sslReq := &SslRequest{}
sslReq.Parse(param)
ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
if err != nil {
- return nil, err
+ e := errno.FromMessage(errno.SslParseError, err.Error())
+ return nil, e
}
ssl.PublicKey = ""
@@ -141,9 +160,7 @@ func SslCheck(param interface{}) ([]byte, error) {
sslDto := &SslDto{}
sslDto.Parse(ssl)
- data := errno.FromMessage(errno.SystemSuccess).ItemResponse(sslDto)
-
- return json.Marshal(data)
+ return sslDto, nil
}
func SslCreate(param interface{}, id string) error {
@@ -153,7 +170,8 @@ func SslCreate(param interface{}, id string) error {
ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
if err != nil {
- return err
+ e := errno.FromMessage(errno.SslParseError, err.Error())
+ return e
}
// first admin api
@@ -162,12 +180,18 @@ func SslCreate(param interface{}, id string) error {
sslReq.Snis = snis
if _, err := sslReq.PutToApisix(id); err != nil {
- return err
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixSslCreateError, err.Error())
+ return e
}
// then mysql
ssl.ID = uuid.FromStringOrNil(id)
+ ssl.Status = 1
+
if err := conf.DB().Create(ssl).Error; err != nil {
- return err
+ return errno.New(errno.DBWriteError, err.Error())
}
return nil
@@ -180,7 +204,8 @@ func SslUpdate(param interface{}, id string) error {
ssl, err := ParseCert(sslReq.PublicKey, sslReq.PrivateKey)
if err != nil {
- return err
+ e := errno.FromMessage(errno.SslParseError, err.Error())
+ return e
}
// first admin api
@@ -189,14 +214,39 @@ func SslUpdate(param interface{}, id string) error {
sslReq.Snis = snis
if _, err := sslReq.PutToApisix(id); err != nil {
- return err
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixSslUpdateError, err.Error())
+ return e
}
// then mysql
ssl.ID = uuid.FromStringOrNil(id)
data := Ssl{PublicKey: ssl.PublicKey, Snis: ssl.Snis, ValidityStart:
ssl.ValidityStart, ValidityEnd: ssl.ValidityEnd}
if err := conf.DB().Model(&ssl).Updates(data).Error; err != nil {
- return err
+ return errno.New(errno.DBWriteError, err.Error())
+ }
+
+ return nil
+}
+
+func SslPatch(param interface{}, id string) error {
+ sslReq := &SslRequest{}
+ sslReq.Parse(param)
+
+ if _, err := sslReq.PatchToApisix(id); err != nil {
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixSslUpdateError, err.Error())
+ return e
+ }
+
+ ssl := Ssl{}
+ ssl.ID = uuid.FromStringOrNil(id)
+ if err := conf.DB().Model(&ssl).Update("status", sslReq.Status).Error;
err != nil {
+ return errno.New(errno.DBWriteError, err.Error())
}
return nil
@@ -207,18 +257,44 @@ func SslDelete(id string) error {
request := &SslRequest{}
request.ID = id
if _, err := request.DeleteFromApisix(); err != nil {
- return err
+ if _, ok := err.(*errno.HttpError); ok {
+ return err
+ }
+ e := errno.New(errno.ApisixSslDeleteError, err.Error())
+ return e
}
// delete from mysql
ssl := &Ssl{}
ssl.ID = uuid.FromStringOrNil(id)
if err := conf.DB().Delete(ssl).Error; err != nil {
- return err
+ return errno.New(errno.DBDeleteError, err.Error())
}
return nil
}
+func (req *SslRequest) PatchToApisix(id string) (*ApisixSslResponse, error) {
+ url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, id)
+ if data, err := json.Marshal(req); err != nil {
+ return nil, err
+ } else {
+ if resp, err := utils.Patch(url, data); err != nil {
+ logger.Error(url)
+ logger.Error(string(data))
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ var arresp ApisixSslResponse
+ if err := json.Unmarshal(resp, &arresp); err != nil {
+ logger.Error(err.Error())
+ return nil, err
+ } else {
+ return &arresp, nil
+ }
+ }
+ }
+}
+
func (req *SslRequest) PutToApisix(rid string) (*ApisixSslResponse, error) {
url := fmt.Sprintf("%s/ssl/%s", conf.BaseUrl, rid)
if data, err := json.Marshal(req); err != nil {
@@ -260,7 +336,10 @@ func (req *SslRequest) DeleteFromApisix()
(*ApisixSslResponse, error) {
}
func ParseCert(crt, key string) (*Ssl, error) {
- // print private key
+ if crt == "" || key == "" {
+ return nil, errors.New("invalid certificate")
+ }
+
certDERBlock, _ := pem.Decode([]byte(crt))
if certDERBlock == nil {
return nil, errors.New("Certificate resolution failed")
@@ -268,7 +347,7 @@ func ParseCert(crt, key string) (*Ssl, error) {
// match
_, err := tls.X509KeyPair([]byte(crt), []byte(key))
if err != nil {
- return nil, err
+ return nil, errors.New("key and cert don't match")
}
x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes)
@@ -277,17 +356,45 @@ func ParseCert(crt, key string) (*Ssl, error) {
return nil, errors.New("Certificate resolution failed")
} else {
ssl := Ssl{}
-
//domain
snis := []byte{}
- if x509Cert.DNSNames == nil || len(x509Cert.DNSNames) < 1 {
+ if x509Cert.DNSNames != nil && len(x509Cert.DNSNames) > 0 {
+ snis, _ = json.Marshal(x509Cert.DNSNames)
+ } else if x509Cert.IPAddresses != nil &&
len(x509Cert.IPAddresses) > 0 {
+ snis, _ = json.Marshal(x509Cert.IPAddresses)
+ } else {
tmp := []string{}
- if x509Cert.Subject.CommonName != "" {
+
+ if x509Cert.Subject.Names != nil &&
len(x509Cert.Subject.Names) > 1 {
+
+ var attributeTypeNames = map[string]string{
+ "2.5.4.6": "C",
+ "2.5.4.10": "O",
+ "2.5.4.11": "OU",
+ "2.5.4.3": "CN",
+ "2.5.4.5": "SERIALNUMBER",
+ "2.5.4.7": "L",
+ "2.5.4.8": "ST",
+ "2.5.4.9": "STREET",
+ "2.5.4.17": "POSTALCODE",
+ }
+
+ for _, tv := range x509Cert.Subject.Names {
+ oidString := tv.Type.String()
+ typeName, ok :=
attributeTypeNames[oidString]
+ if ok && typeName == "CN" {
+ valueString :=
fmt.Sprint(tv.Value)
+ tmp = append(tmp, valueString)
+ }
+
+ }
+ }
+
+ if len(tmp) < 1 && x509Cert.Subject.CommonName != "" {
tmp = []string{x509Cert.Subject.CommonName}
}
+
snis, _ = json.Marshal(tmp)
- } else {
- snis, _ = json.Marshal(x509Cert.DNSNames)
}
ssl.Snis = string(snis)
diff --git a/api/service/ssl_test.go b/api/service/ssl_test.go
new file mode 100644
index 0000000..3b59fce
--- /dev/null
+++ b/api/service/ssl_test.go
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package service
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/satori/go.uuid"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/apisix/manager-api/errno"
+)
+
+var (
+ u1 = uuid.NewV4()
+ u2 = uuid.NewV4()
+)
+
+func TestSslParseCert(t *testing.T) {
+ ssl, err := ParseCert("", "")
+
+ assert := assert.New(t)
+ assert.Nil(ssl)
+ assert.Equal("invalid certificate", err.Error())
+
+ //KeyPair fail
+
+ cert := `-----BEGIN CERTIFICATE-----
+MIIEWjCCAsKgAwIBAgIRAMLLNCKEvgEQL22Hpox6E1kwDQYJKoZIhvcNAQELBQAw
+fzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSowKAYDVQQLDCFqdW54
+dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikxMTAvBgNVBAMMKG1rY2VydCBq
+dW54dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikwHhcNMTkwNjAxMDAwMDAw
+WhcNMzAwNjA5MTA0MjA1WjBVMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQg
+Y2VydGlmaWNhdGUxKjAoBgNVBAsMIWp1bnh1Y2hlbkBqdW54dWRlQWlyIChqdW54
+dSBjaGVuKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2lg+vzHkRI
+Cs8PHv6UVxXFbrL6wlsdurOICkW5daKUJyzUZQZl11CK9SWOk8vAc7j3pQ7Mz15r
+hfQB558WHzI/XXbZ1NrZrTpLaL0fWW5n4hIE8EbYf3Hy/xM8gRUXsWMEexq2WC/R
+PfTCQIZ85vUSANS72E5rHdba3Y5IMr8bn/NUg1sm2LxZQmZV6tBOpYnibyj7bXxw
+8kxr4w+B/5jDBPmwL59bdoatEs1FjdHzz+fbW1K4NdHZZEotYqkQhCS09JnwGswd
+Ariy+Is44kt/gtw7nVWmuV/eQaxPEHVE4Bwvdmv11IsPsj6hif23gXjXLIHx66CY
+/S4I/P0allkCAwEAAaN7MHkwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsG
+AQUFBwMBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUVPhGyY/h6etoAqzb298k
+JGJzx1wwIwYDVR0RBBwwGoIJdGVzdDMuY29tgg13d3cudGVzdDMuY29tMA0GCSqG
+SIb3DQEBCwUAA4IBgQCgb5wOMzkD1/tgrjwE7Cj1NvX7/p/JIQVVtvtnnypyXGNn
+VL+q4oB9WzvOvMcTCiKKHqg9jCiu/KFFHy7nRzj7KPhU/o9M7qLNwLJjfUOtPYUm
+rS62kEXlj5L5+UJjiGABGfLllxMwwTkAFbdSUSB1awzoafPn5+g+qABXgkF/EN2I
++IJcJCqg3IO30n4MMhqNx3IbqIohD3p5GzjQqnuQSrC/HJEsUuIlMCHPJ1GVbbrd
+RnSySMcbv2jThP53JVIe+0HHvcujb2pDQ4RcCSaN3OXaZDYVqoSR04+amotGwiWO
+DY/4LTWFJkfoWnv1kg2/AllMpsXB+1u+O+x6qWzBw2hXP5AM+8KIoJ2/Mb13TsN9
+qhrGep+SfhjARH068ZjaS2zQC2Uvc+SrEGXfITPIkstRELxIV9Lmjl9lwpAOJrte
+4TDjYhBS20j6mt3dUyEBnPfkpcOeLYZNS1sK66MRfzJ9MowdTcWyvMzFHjSjfWPY
++4scQdmkFkCulnzylak=
+-----END CERTIFICATE-----`
+ key := `-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC25tQs8GRxdWUR
+c4kjhmJmrxQgCcoN4VTHyBbdbnZFPgxCULAr/2e54ooatMWh9DEdrgpi3EfQXqDV
+okzKrSfJVZqmS1qOcgaSkgz5/puKcF31bXUgSVw1qGEjXbpQcyzZp95pPaErXCq3
+7bKST5d0TO8asNebON+jZZkpa5NA+qFOOWdx0Y6UWG0G/RzhYyRRU9B6+SBwOLa8
+c9+07vxjjAOXfr5Terp0lS9xNOVcjnHA48XPq2y8cJstbVjQI8Ls1HlzJoeJz+lt
+FFxd8nMQ30NoJYoVXewUEjuTibQdEpUY8m5zuNqYKOax4McpoTouKl2BWIuWr10Y
+Wrtw9XWlAgMBAAECggEAdoDsbAl9Kr3jNAFlk2zYiKtbIL72+TNL2P1dQy701jwz
+eSwKWRdsP1X2IQOLITm0MQS4mgEbTnhhQMmdc5vpMVuTjbc4/x4GACU83yUF5haT
+6hZ3Uun1IpbsCRwLQWC+aG+JfSp/KDbZPr51erKy8JmAOgzmRR3+WEHVkK6wg+JF
+qDha7Gnq2xgzmutGLBUb9BFgL7bbNyf3V4d+HbgYPjQossfpLnTIrsmKce36r5U8
+pF6Uf69EUs34N9w98E6mEGY12nY4jOKZMeRPR1FIGGK7Y3+7fkd2+pYF00s7uSUq
+/j8DdxxykqvZkE1lBZJBwxk9Ty4PBq2lNKdq6pXtAQKBgQDu12vVuenRCJEsK2FY
+puYlfoDiQpIYzK9Bme+eDsPcVyOQ9q+Tf4QASZN81InQsanQ2ovcbmSpB52/ZvjU
+rVX7RnIEcZ6jB1VUcCU/QOEg0kqTkCo3SfyeawF8iPyuLSbb5ZBufFMq6nOyOWco
+hg4EqQIg3rcKw3lW8NT6DqqRxQKBgQDECpuhfEMYUslfDlSbS8V/vSXjD2xzRFt3
+SL87xQXwZOLRPYiLdRGb/WP2L/PGE8K1XUMz/geeFeAdrzRxJ1JtCn/K0UyMxSnw
+TJEm6CFfsOJXOq4yC5gosNYw3xBc9NKtsGLrAlWOBLPdeO1wdrM6BYfs6AP3X49r
+67Y7AujyYQKBgQCMSV//c2nQ6/VJOlm9VprL3xgYzf0+L8uo/p/t+MI2Q8CSPzM1
+sap4+L52jeg8+n3CPPv1h6n8Vorjh7oUQZPFOcVyssH5BC+snwphstwJCTvgnMcP
+HpgQ/M0sttGkBMVUV+yT2NaI2JkIUAs1lDfbqOGlKOvemJ5G4MJX9hFd+QKBgQDC
+oe2F1EMg4QCAWU/yprW8buQwnF2FyzYsJZOHGcMdumveZYMtQdtrzZTzFQSngXLs
+cV2JPwn9D6bkkdA1D18sVyItEMM5d359zubFg+2ufYUaKW5MzWoR7A+bkbtDLuYD
+/30V6clbKJwSpD7IS3EBiAA9WtSlQsC32tuflvIDwQKBgAqJAqrempkVGA2rvTX5
+7uWCAiqUKX859kmIuQV9RTQ7qfQRWhckuAjeTdTq6tWKFkzaVaRCXb+tRi7eqVsz
+R/us0LXwHez2LacSVudO7g/LMY3yDxVL2iFLVA/x1FBqjAgjWAEfnc7PmZSK2B3g
+fpf+iWh15t6I4nuyEmMFmtVr
+-----END PRIVATE KEY-----`
+ ssl1, err := ParseCert(cert, key)
+
+ assert.Nil(ssl1)
+ assert.Equal("key and cert don't match", err.Error())
+
+ // ip
+
+ cert = `-----BEGIN CERTIFICATE-----
+MIIERjCCAq6gAwIBAgIRALI5vYiIHdtNd4ocLsZ9i/IwDQYJKoZIhvcNAQELBQAw
+fzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSowKAYDVQQLDCFqdW54
+dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikxMTAvBgNVBAMMKG1rY2VydCBq
+dW54dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikwHhcNMTkwNjAxMDAwMDAw
+WhcNMzAwNjAzMDEyODQ5WjBVMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQg
+Y2VydGlmaWNhdGUxKjAoBgNVBAsMIWp1bnh1Y2hlbkBqdW54dWRlQWlyIChqdW54
+dSBjaGVuKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALbm1CzwZHF1
+ZRFziSOGYmavFCAJyg3hVMfIFt1udkU+DEJQsCv/Z7niihq0xaH0MR2uCmLcR9Be
+oNWiTMqtJ8lVmqZLWo5yBpKSDPn+m4pwXfVtdSBJXDWoYSNdulBzLNmn3mk9oStc
+KrftspJPl3RM7xqw15s436NlmSlrk0D6oU45Z3HRjpRYbQb9HOFjJFFT0Hr5IHA4
+trxz37Tu/GOMA5d+vlN6unSVL3E05VyOccDjxc+rbLxwmy1tWNAjwuzUeXMmh4nP
+6W0UXF3ycxDfQ2glihVd7BQSO5OJtB0SlRjybnO42pgo5rHgxymhOi4qXYFYi5av
+XRhau3D1daUCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsG
+AQUFBwMBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUVPhGyY/h6etoAqzb298k
+JGJzx1wwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAYEAowBgpVs+
+pZ4PeT2mVx79thwh02AzinmNbMQMboHK9RaRSZHxLHiVjY7LR1qn5zjLcUcZHel7
+96bka6vlufYzWHSjtKwAchDVVPVK6tzpfHnpLzNLShx0LjIl0EvjrHv5mHIx8R1T
+WKQGxwdTzIxGMgrziuTNc1LvS+yoJElWe07PeRSdSPKUNKQQWqaukzqw3JZaLQkm
+8MaqrzQwNEsCbeojz/ME3e+SzpSsqXgW0x5Og4TyEnIen/q7OzmRzrkHR40Hu+j4
+aW287l3ywIU0pGl/sY2bmyOYCgpZ5w2utfdd7167Doy4vHFKJD3GuN3i+W1f7PyF
+gdUFUQTr6HO4sfQXeZoitzl2Dvr360sb9BBDLAoFiV6J5bAV4algVz45kSpJ7VIm
+pfS8+BUcZHWbn35M9mx2dyoNvIajcET545D60wcttqw4TRh4ljsE69GCU1swiwxH
+hXSkf958injuwYuxv0b1k9qGm6znJc+tRcn35H6x0ff/HwuBIj3TsEXo
+-----END CERTIFICATE-----`
+ key = `-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC25tQs8GRxdWUR
+c4kjhmJmrxQgCcoN4VTHyBbdbnZFPgxCULAr/2e54ooatMWh9DEdrgpi3EfQXqDV
+okzKrSfJVZqmS1qOcgaSkgz5/puKcF31bXUgSVw1qGEjXbpQcyzZp95pPaErXCq3
+7bKST5d0TO8asNebON+jZZkpa5NA+qFOOWdx0Y6UWG0G/RzhYyRRU9B6+SBwOLa8
+c9+07vxjjAOXfr5Terp0lS9xNOVcjnHA48XPq2y8cJstbVjQI8Ls1HlzJoeJz+lt
+FFxd8nMQ30NoJYoVXewUEjuTibQdEpUY8m5zuNqYKOax4McpoTouKl2BWIuWr10Y
+Wrtw9XWlAgMBAAECggEAdoDsbAl9Kr3jNAFlk2zYiKtbIL72+TNL2P1dQy701jwz
+eSwKWRdsP1X2IQOLITm0MQS4mgEbTnhhQMmdc5vpMVuTjbc4/x4GACU83yUF5haT
+6hZ3Uun1IpbsCRwLQWC+aG+JfSp/KDbZPr51erKy8JmAOgzmRR3+WEHVkK6wg+JF
+qDha7Gnq2xgzmutGLBUb9BFgL7bbNyf3V4d+HbgYPjQossfpLnTIrsmKce36r5U8
+pF6Uf69EUs34N9w98E6mEGY12nY4jOKZMeRPR1FIGGK7Y3+7fkd2+pYF00s7uSUq
+/j8DdxxykqvZkE1lBZJBwxk9Ty4PBq2lNKdq6pXtAQKBgQDu12vVuenRCJEsK2FY
+puYlfoDiQpIYzK9Bme+eDsPcVyOQ9q+Tf4QASZN81InQsanQ2ovcbmSpB52/ZvjU
+rVX7RnIEcZ6jB1VUcCU/QOEg0kqTkCo3SfyeawF8iPyuLSbb5ZBufFMq6nOyOWco
+hg4EqQIg3rcKw3lW8NT6DqqRxQKBgQDECpuhfEMYUslfDlSbS8V/vSXjD2xzRFt3
+SL87xQXwZOLRPYiLdRGb/WP2L/PGE8K1XUMz/geeFeAdrzRxJ1JtCn/K0UyMxSnw
+TJEm6CFfsOJXOq4yC5gosNYw3xBc9NKtsGLrAlWOBLPdeO1wdrM6BYfs6AP3X49r
+67Y7AujyYQKBgQCMSV//c2nQ6/VJOlm9VprL3xgYzf0+L8uo/p/t+MI2Q8CSPzM1
+sap4+L52jeg8+n3CPPv1h6n8Vorjh7oUQZPFOcVyssH5BC+snwphstwJCTvgnMcP
+HpgQ/M0sttGkBMVUV+yT2NaI2JkIUAs1lDfbqOGlKOvemJ5G4MJX9hFd+QKBgQDC
+oe2F1EMg4QCAWU/yprW8buQwnF2FyzYsJZOHGcMdumveZYMtQdtrzZTzFQSngXLs
+cV2JPwn9D6bkkdA1D18sVyItEMM5d359zubFg+2ufYUaKW5MzWoR7A+bkbtDLuYD
+/30V6clbKJwSpD7IS3EBiAA9WtSlQsC32tuflvIDwQKBgAqJAqrempkVGA2rvTX5
+7uWCAiqUKX859kmIuQV9RTQ7qfQRWhckuAjeTdTq6tWKFkzaVaRCXb+tRi7eqVsz
+R/us0LXwHez2LacSVudO7g/LMY3yDxVL2iFLVA/x1FBqjAgjWAEfnc7PmZSK2B3g
+fpf+iWh15t6I4nuyEmMFmtVr
+-----END PRIVATE KEY-----`
+ ssl2, err := ParseCert(cert, key)
+
+ assert.Nil(err)
+ assert.Equal("[\"127.0.0.1\"]", ssl2.Snis)
+
+ // v1 multi domain
+ cert = `-----BEGIN CERTIFICATE-----
+MIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD
+TjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl
+eTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv
+bTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0
+WjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5
+MQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t
+MQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZIhvcNAQEB
+BQADgY0AMIGJAoGBANHMrKlfFzJbyYuD0YveK2mOOXR9zXi+vC5lW6RaoyKjx5AL
+yIXQWXURGVnxw1+xbmxWN1MXZyAP7eJYFPa0PIJvW0kbyHkJt/TrCyBLVOqpTqvE
+kDAIde9Fx83556sXD43Oq93lyBraXmR+fXuoLxJQQLhALW1tOg1X3VrxKYXNAgMB
+AAEwDQYJKoZIhvcNAQELBQADgYEAwJ7qV0Tj6JXR035ySVSBG1KBF19DVmMYRKdO
+SAU1j437q+ktTcEWSA0CkH6rg53tP4V1h0tzdhCxisivYynngjtEcZfsrwdIrsSg
+cmOBZ+KTRyZ2fLgH4F8Naz5hBrwmR8ZIG46feVOV/swJzz4BNaXGj1oATWkLMA3c
+Sf0G+aI=
+-----END CERTIFICATE-----`
+ key = `-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDRzKypXxcyW8mLg9GL3itpjjl0fc14vrwuZVukWqMio8eQC8iF
+0Fl1ERlZ8cNfsW5sVjdTF2cgD+3iWBT2tDyCb1tJG8h5Cbf06wsgS1TqqU6rxJAw
+CHXvRcfN+eerFw+Nzqvd5cga2l5kfn17qC8SUEC4QC1tbToNV91a8SmFzQIDAQAB
+AoGBAJIL/y4wqf8+ckES1G6fjG0AuvJjGQQzEuDhYjg5eFMG3EdkTIUKkxuxeYpp
+iG43H/1+zyiipAFn1Vu5oW5T7cJEgC1YA39dERT605S5BrNWWHoZsgH+qmLoq7X+
+jXMlmCagwlgwhUWMU2M1/LUbAl42384dK9u3EwcCgS//sFuBAkEA6mK52/Z03PB3
+0sS14eN7xFl96yc/NcneJ7Vy5APT0KGLo0j2S8gpOVW9EYrrzDzWgQ8FLIeed2Zw
+Z4ATksgRXQJBAOUlh5VJkyMdMiDEeJgK9QKtJkuiLZFAzZiWAUqjvSG2j8tWX/iN
+veI1sXCPyQSKoWPN74+23KWL+nW+mUzkzzECQFf+UIB/+keoD5QVPaNcX+7LGjba
+OSTccIa/3C42MaM1wtK+ZZj1wGRCCAU5/mRiwrUZCnw5PgjdcH2q265TZhECQASY
+JgnGOd8AXNrvVYOm5JazJgtqKwO4iua+SzRV6Bre8C8hgjcXkHESpoYdO+iNZwL7
+RAxbnDzte44UzjoOdGECQGtkrBffiyMaQv6LM/6Fa5TXHb1kPtLGIjFSygR3eTYI
+gHG78R5ac0dzhbyKaOo6cbj7CJVkbBh4BNW94tBZE/I=
+-----END RSA PRIVATE KEY-----`
+
+ ssl3, err := ParseCert(cert, key)
+
+ assert.Nil(err)
+ assert.Equal("[\"a.com\",\"b.com\",\"c.com\"]", ssl3.Snis)
+}
+
+func TestSslCurd(t *testing.T) {
+ //test3.com
+ param := []byte(`{
+ "cert": "-----BEGIN
CERTIFICATE-----\nMIIEWjCCAsKgAwIBAgIRAMLLNCKEvgEQL22Hpox6E1kwDQYJKoZIhvcNAQELBQAw\nfzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSowKAYDVQQLDCFqdW54\ndWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikxMTAvBgNVBAMMKG1rY2VydCBq\ndW54dWNoZW5AanVueHVkZUFpciAoanVueHUgY2hlbikwHhcNMTkwNjAxMDAwMDAw\nWhcNMzAwNjA5MTA0MjA1WjBVMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQg\nY2VydGlmaWNhdGUxKjAoBgNVBAsMIWp1bnh1Y2hlbkBqdW54dWRlQWlyIChqdW54\ndSBjaGVuKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2l
[...]
+ "key": "-----BEGIN PRIVATE
KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9pYPr8x5ESArP\nDx7+lFcVxW6y+sJbHbqziApFuXWilCcs1GUGZddQivUljpPLwHO496UOzM9ea4X0\nAeefFh8yP1122dTa2a06S2i9H1luZ+ISBPBG2H9x8v8TPIEVF7FjBHsatlgv0T30\nwkCGfOb1EgDUu9hOax3W2t2OSDK/G5/zVINbJti8WUJmVerQTqWJ4m8o+218cPJM\na+MPgf+YwwT5sC+fW3aGrRLNRY3R88/n21tSuDXR2WRKLWKpEIQktPSZ8BrMHQK4\nsviLOOJLf4LcO51Vprlf3kGsTxB1ROAcL3Zr9dSLD7I+oYn9t4F41yyB8eugmP0u\nCPz9GpZZAgMBAAECggEAZ2+8SVgb/QASLSdBL3d3HB/IJgSRNyM67qrXd
[...]
+ }`)
+
+ err := SslCreate(param, u1.String())
+
+ assert := assert.New(t)
+ assert.Nil(err)
+
+ //get item
+ ssl, err := SslItem(u1.String())
+ assert.Nil(err)
+ assert.Equal(uint64(1), ssl.Status)
+ assert.Equal(2, len(ssl.Snis))
+ for _, dm := range ssl.Snis {
+ assert.Equal(true, strings.Contains(dm, "test3.com"))
+ }
+
+ //a.com b.com fail
+ param = []byte(`{
+ "cert": "-----BEGIN
CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZI
[...]
+ "key": "-----BEGIN RSA PRIVATE
KEY-----\nMIICXAIBAAKBgQDRzKypXxcyW8mLg9GL3itpjjl0fc14vrwuZVukWqMio8eQC8iF\n0Fl1ERlZ8cNfsW5sVjdTF2cgD+3iWBT2tDyCb1tJG8h5Cbf06wsgS1TqqU6rxJAw\nCHXvRcfN+eerFw+Nzqvd5cga2l5kfn17qC8SUEC4QC1tbToNV91a8SmFzQIDAQAB\nAoGBAJIL/y4wqf8+ckES1G6fjG0AuvJjGQQzEuDhYjg5eFMG3EdkTIUKkxuxeYpp\niG43H/1+zyiipAFn1Vu5oW5T7cJEgC1YA39dERT605S5BrNWWHoZsgH+qmLoq7X+\njXMlmCagwlgwhUWMU2M1/LUbAl42384dK9u3EwcCgS//sFuBAkEA6mK52/Z03PB3\n0sS14eN7xFl96yc/NcneJ7Vy5APT0KGLo0j2S8gpOVW9EYrrzDzWg
[...]
+ }`)
+
+ err = SslCreate(param, u1.String())
+ assert.NotNil(err)
+ assert.Equal(errno.DBWriteError.Code, err.(*errno.ManagerError).Code)
+
+ //a.com b.com ok
+ param = []byte(`{
+ "cert": "-----BEGIN
CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZI
[...]
+ "key": "-----BEGIN RSA PRIVATE
KEY-----\nMIICXAIBAAKBgQDRzKypXxcyW8mLg9GL3itpjjl0fc14vrwuZVukWqMio8eQC8iF\n0Fl1ERlZ8cNfsW5sVjdTF2cgD+3iWBT2tDyCb1tJG8h5Cbf06wsgS1TqqU6rxJAw\nCHXvRcfN+eerFw+Nzqvd5cga2l5kfn17qC8SUEC4QC1tbToNV91a8SmFzQIDAQAB\nAoGBAJIL/y4wqf8+ckES1G6fjG0AuvJjGQQzEuDhYjg5eFMG3EdkTIUKkxuxeYpp\niG43H/1+zyiipAFn1Vu5oW5T7cJEgC1YA39dERT605S5BrNWWHoZsgH+qmLoq7X+\njXMlmCagwlgwhUWMU2M1/LUbAl42384dK9u3EwcCgS//sFuBAkEA6mK52/Z03PB3\n0sS14eN7xFl96yc/NcneJ7Vy5APT0KGLo0j2S8gpOVW9EYrrzDzWg
[...]
+ }`)
+
+ err = SslCreate(param, u2.String())
+ assert.Nil(err)
+
+ //list
+ count, list, err := SslList(2, 1, -1, 0, 0, "")
+ assert.Equal(true, count >= 2)
+ assert.Equal(1, len(list))
+
+ // patch
+ param = []byte(`{
+ "status": 0
+ }`)
+ err = SslPatch(param, u1.String())
+ assert.Nil(err)
+
+ ssl, err = SslItem(u1.String())
+ assert.Equal(uint64(0), ssl.Status)
+
+ //update
+ param = []byte(`{
+ "cert": "-----BEGIN
CERTIFICATE-----\nMIICcTCCAdoCCQDQoPEll/bQizANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJD\nTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5MQ4wDAYDVQQKDAVteWtl\neTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29tMQ4wDAYDVQQDDAViLmNv\nbTEOMAwGA1UEAwwFYy5jb20wHhcNMjAwNjE3MDk1MDA0WhcNMzAwNjE1MDk1MDA0\nWjB9MQswCQYDVQQGEwJDTjEOMAwGA1UECAwFbXlrZXkxDjAMBgNVBAcMBW15a2V5\nMQ4wDAYDVQQKDAVteWtleTEOMAwGA1UECwwFbXlrZXkxDjAMBgNVBAMMBWEuY29t\nMQ4wDAYDVQQDDAViLmNvbTEOMAwGA1UEAwwFYy5jb20wgZ8wDQYJKoZI
[...]
+ "key": "-----BEGIN RSA PRIVATE
KEY-----\nMIICXAIBAAKBgQDRzKypXxcyW8mLg9GL3itpjjl0fc14vrwuZVukWqMio8eQC8iF\n0Fl1ERlZ8cNfsW5sVjdTF2cgD+3iWBT2tDyCb1tJG8h5Cbf06wsgS1TqqU6rxJAw\nCHXvRcfN+eerFw+Nzqvd5cga2l5kfn17qC8SUEC4QC1tbToNV91a8SmFzQIDAQAB\nAoGBAJIL/y4wqf8+ckES1G6fjG0AuvJjGQQzEuDhYjg5eFMG3EdkTIUKkxuxeYpp\niG43H/1+zyiipAFn1Vu5oW5T7cJEgC1YA39dERT605S5BrNWWHoZsgH+qmLoq7X+\njXMlmCagwlgwhUWMU2M1/LUbAl42384dK9u3EwcCgS//sFuBAkEA6mK52/Z03PB3\n0sS14eN7xFl96yc/NcneJ7Vy5APT0KGLo0j2S8gpOVW9EYrrzDzWg
[...]
+ }`)
+
+ err = SslUpdate(param, u1.String())
+ assert.Nil(err)
+
+ ssl, _ = SslItem(u1.String())
+ assert.Equal(3, len(ssl.Snis))
+
+ //delete
+ err = SslDelete(u1.String())
+ assert.Nil(err)
+
+ count2, _, err := SslList(2, 1, -1, 0, 0, "")
+ assert.Equal(count2, count-1)
+
+ err = SslDelete(u2.String())
+ assert.Nil(err)
+}