/* * Copyright 2021 Synology Inc. */ package webapi import ( "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "regexp" "github.com/SynologyOpenSource/synology-csi/pkg/logger" log "github.com/sirupsen/logrus" ) type DSM struct { Ip string Port int Username string Password string Sid string Https bool Controller string TLSCACert string TLSServerName string InsecureSkipVerify bool SystemInfo } type SystemInfo struct { Hostname string SupportNvmeof bool FirmwareVer string } type errData struct { Code int `json:"code"` } type dsmApiResp struct { Success bool `json:"success"` Err errData `json:"error"` } type Response struct { StatusCode int ErrorCode int Success bool Data interface{} } func (dsm *DSM) newHTTPClient() (*http.Client, error) { if !dsm.Https { return &http.Client{}, nil } tlsCfg := &tls.Config{} if dsm.TLSServerName != "" { tlsCfg.ServerName = dsm.TLSServerName } if dsm.InsecureSkipVerify { if dsm.TLSCACert != "" { log.Warnf("tlsCACert is configured but will be ignored for DSM %s because insecureSkipVerify is true.", dsm.Ip) } log.Warnf("TLS certificate verification is disabled for DSM %s. "+ "This is insecure; provide tlsCACert instead.", dsm.Ip) tlsCfg.InsecureSkipVerify = true } else if dsm.TLSCACert != "" { pool, err := x509.SystemCertPool() if err != nil { log.Warnf("Failed to load system CA pool for DSM %s, falling back to empty pool: %v", dsm.Ip, err) pool = x509.NewCertPool() } if ok := pool.AppendCertsFromPEM([]byte(dsm.TLSCACert)); !ok { return nil, fmt.Errorf("failed to parse TLS CA certificate for DSM %s", dsm.Ip) } tlsCfg.RootCAs = pool } tr := http.DefaultTransport.(*http.Transport).Clone() tr.TLSClientConfig = tlsCfg return &http.Client{Transport: tr}, nil } func (dsm *DSM) sendRequest(data string, apiTemplate interface{}, params url.Values, cgiPath string) (Response, error) { resp, err := dsm.sendRequestWithoutConnectionCheck(data, apiTemplate, params, cgiPath) if err != nil && (resp.ErrorCode == 105 || resp.ErrorCode == 106 || resp.ErrorCode == 119) { // 105: WEBAPI_ERR_NO_PERMISSION, 106: session timeout, 119: WEBAPI_ERR_SID_NOT_FOUND // Re-login if err := dsm.Login(); err != nil { return Response{}, fmt.Errorf("Failed to re-login to DSM: [%s]. err: %v", dsm.Ip, err) } log.Info("Re-login succeeded.") return dsm.sendRequestWithoutConnectionCheck(data, apiTemplate, params, cgiPath) } return resp, err } func (dsm *DSM) sendRequestWithoutConnectionCheck(data string, apiTemplate interface{}, params url.Values, cgiPath string) (Response, error) { client, err := dsm.newHTTPClient() if err != nil { return Response{}, err } var req *http.Request var cgiUrl string // Ex: http://10.12.12.14:5000/webapi/auth.cgi if dsm.Https { cgiUrl = fmt.Sprintf("https://%s:%d/%s", dsm.Ip, dsm.Port, cgiPath) } else { cgiUrl = fmt.Sprintf("http://%s:%d/%s", dsm.Ip, dsm.Port, cgiPath) } baseUrl, err := url.Parse(cgiUrl) if err != nil { return Response{}, err } baseUrl.RawQuery = params.Encode() if logger.WebapiDebug { log.Debugln(baseUrl.RawQuery) } if data != "" { req, err = http.NewRequest("POST", baseUrl.String(), nil) } else { req, err = http.NewRequest("GET", baseUrl.String(), nil) } if dsm.Sid != "" { cookie := http.Cookie{Name: "id", Value: dsm.Sid} req.AddCookie(&cookie) } resp, err := client.Do(req) if err != nil { return Response{}, err } defer resp.Body.Close() // For debug print text body var bodyText []byte if logger.WebapiDebug { bodyText, err = ioutil.ReadAll(resp.Body) if err != nil { return Response{}, err } s := string(bodyText) log.Debugln(s) } if resp.StatusCode != 200 && resp.StatusCode != 302 { return Response{}, fmt.Errorf("Bad response status code: %d", resp.StatusCode) } // Strip data json data from response type envelop struct { dsmApiResp Data json.RawMessage `json:"data"` } e := envelop{} var outResp Response if logger.WebapiDebug { if err := json.Unmarshal(bodyText, &e); err != nil { return Response{}, err } } else { decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&e); err != nil { return Response{}, err } } outResp.Success = e.Success outResp.ErrorCode = e.Err.Code outResp.StatusCode = resp.StatusCode if !e.Success { return outResp, fmt.Errorf("DSM Api error. Error code:%d", outResp.ErrorCode) } if e.Data != nil { if err := json.Unmarshal(e.Data, apiTemplate); err != nil { return Response{}, err } } outResp.Data = apiTemplate if err != nil { return Response{}, err } return outResp, nil } // Login by given user name and password func (dsm *DSM) Login() error { params := url.Values{} params.Add("api", "SYNO.API.Auth") params.Add("method", "login") params.Add("version", "3") params.Add("account", dsm.Username) params.Add("passwd", dsm.Password) params.Add("format", "sid") type LoginResp struct { Sid string `json:"sid"` } resp, err := dsm.sendRequestWithoutConnectionCheck("", &LoginResp{}, params, "webapi/auth.cgi") if err != nil { r, _ := regexp.Compile("passwd=.*&") temp := r.ReplaceAllString(err.Error(), "") return fmt.Errorf("%s", temp) } loginResp, ok := resp.Data.(*LoginResp) if !ok { return fmt.Errorf("Failed to assert response to %T", &LoginResp{}) } dsm.Sid = loginResp.Sid return nil } // Logout on current IP and reset the synoToken func (dsm *DSM) Logout() error { params := url.Values{} params.Add("api", "SYNO.API.Auth") params.Add("method", "logout") params.Add("version", "1") _, err := dsm.sendRequestWithoutConnectionCheck("", &struct{}{}, params, "webapi/entry.cgi") if err != nil { return err } dsm.Sid = "" return nil }