diff --git a/cmd/get.go b/cmd/get.go index 51719ac..99ee7e1 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -4,7 +4,6 @@ import ( "fmt" "io/ioutil" "os" - "path" "path/filepath" "regexp" "strings" @@ -263,7 +262,7 @@ func (c *getCmd) extractFiles(files []string) error { var mErr error for _, file := range files { - if strings.ToLower(filepath.Ext(file)) == ".tar.gz" { + if !strings.HasSuffix(strings.ToLower(file), ".tar.gz") { continue } @@ -277,14 +276,14 @@ func (c *getCmd) extractFiles(files []string) error { err := unpacker.Extract(file, extractDir) if err != nil { log.Errorf(err.Error()) - multierr.Append(mErr, err) + mErr = multierr.Append(mErr, err) continue } if c.Rename { err := renameFiles(extractDir) if err != nil { - multierr.Append(mErr, err) + mErr = multierr.Append(mErr, err) continue } @@ -409,5 +408,5 @@ func getAbsolutePath(p string) string { if err != nil { log.Errorf("Error getting current path %s", err.Error()) } - return path.Join(dir, p) + return filepath.Join(dir, p) } diff --git a/downloader/client.go b/downloader/client.go index 87b8848..f1bedf0 100644 --- a/downloader/client.go +++ b/downloader/client.go @@ -2,11 +2,12 @@ package downloader import ( "bytes" - "encoding/json" "fmt" "io" "net/http" "net/http/cookiejar" + "net/url" + "regexp" "strings" "sync" @@ -20,9 +21,12 @@ var log = logos.New("github.com/v8platform/oneget/downloader").Sugar() const ( projectHrefPrefix = "/project/" + projectHrefSuffix = "?allUpdates=true" tempFileSuffix = ".d1c" ) +var executionRe = regexp.MustCompile(`name="execution"\s+value="([^"]+)"`) + func NewClient(loginUrl string, baseUrl string, login string, password string) (*Client, error) { cj, _ := cookiejar.New(nil) @@ -35,16 +39,10 @@ func NewClient(loginUrl string, baseUrl string, login string, password string) ( cookie: cj, } - url, err := c.getAuthTicketURL(baseUrl) - if err != nil { - return nil, err - } - - loginResp, err := c.Get(url) + err := c.casLogin() if err != nil { return nil, err } - defer loginResp.Body.Close() return c, nil } @@ -57,74 +55,75 @@ type Client struct { baseUrl string } -func (c *Client) getAuthTicketURL(url string) (string, error) { +// casLogin выполняет аутентификацию через CAS form-based login. +// 1. GET login page — получить execution token и session cookies +// 2. POST login form — аутентификация и установка CAS cookies +func (c *Client) casLogin() error { + serviceURL := c.baseUrl + "/public/security_check" + casLoginURL := c.loginUrl + "/login?service=" + url.QueryEscape(serviceURL) - type loginParams struct { - Login string `json:"login"` - Password string `json:"password"` - ServiceNick string `json:"serviceNick"` + // Шаг 1: GET страницу логина для получения execution token + log.Debugf("CAS login: getting login page from %s", casLoginURL) + req, err := http.NewRequest("GET", casLoginURL, nil) + if err != nil { + return fmt.Errorf("CAS login: create request error: %s", err.Error()) } - type ticket struct { - Ticket string `json:"ticket"` + resp, err := c.doRequest(req) + if err != nil { + return fmt.Errorf("CAS login: get login page error: %s", err.Error()) } + defer resp.Body.Close() - ticketUrl := c.loginUrl + "/rest/public/ticket/get" - postBody, err := json.Marshal( - loginParams{c.login, c.password, url}) + body, err := readBody(resp.Body) if err != nil { - return "", err + return fmt.Errorf("CAS login: read login page error: %s", err.Error()) } - buf := bytes.NewBuffer(postBody) - defer put(buf) - req, err := http.NewRequest("POST", ticketUrl, buf) - - if err != nil { - return "", err + matches := executionRe.FindSubmatch(body) + if matches == nil { + return fmt.Errorf("CAS login: execution token not found on login page") } + executionToken := string(matches[1]) + log.Debugf("CAS login: got execution token (length=%d)", len(executionToken)) - req.SetBasicAuth(c.login, c.password) - req.Header.Set("Content-Type", "application/json") - resp, err := c.doRequest(req) + // Шаг 2: POST form с credentials + formData := url.Values{ + "username": {c.login}, + "password": {c.password}, + "execution": {executionToken}, + "_eventId": {"submit"}, + "geolocation": {""}, + } + req, err = http.NewRequest("POST", casLoginURL, strings.NewReader(formData.Encode())) if err != nil { - return "", err + return fmt.Errorf("CAS login: create POST request error: %s", err.Error()) } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusOK: - - var ticketData ticket - err := bodyToJSON(resp.Body, &ticketData) - if err != nil { - return "", err - } - - return fmt.Sprintf(loginURL+"/ticket/auth?token=%s", ticketData.Ticket), nil - - default: - - type ErrorRespond struct { - Timestamp string `json:"timestamp"` - Status int `json:"status"` - Error string `json:"error"` - Exception string `json:"exception"` - Message string `json:"message"` - Path string `json:"path"` - } + resp2, err := c.doRequest(req) + if err != nil { + return fmt.Errorf("CAS login: POST error: %s", err.Error()) + } + defer resp2.Body.Close() - var errData ErrorRespond + // Проверяем что аутентификация прошла — финальный URL не должен быть страницей логина + finalURL := resp2.Request.URL.String() + log.Debugf("CAS login: final URL after auth: %s", finalURL) - err := bodyToJSON(resp.Body, &errData) - if err != nil { - return "", err + if strings.Contains(finalURL, "login.1c.ru/login") { + // Читаем тело чтобы проверить ошибку + body2, _ := readBody(resp2.Body) + if strings.Contains(string(body2), "Неверный логин или пароль") || + strings.Contains(string(body2), "Incorrect login or password") { + return fmt.Errorf("CAS login: неверный логин или пароль") } - - return "", fmt.Errorf("%s: %s", errData.Error, errData.Message) + return fmt.Errorf("CAS login: аутентификация не удалась, финальный URL: %s", finalURL) } + + log.Debugf("CAS login: authenticated successfully") + return nil } func (c *Client) Get(getUrl string) (*http.Response, error) { @@ -140,8 +139,6 @@ func (c *Client) Get(getUrl string) (*http.Response, error) { return nil, err } - req.SetBasicAuth(c.login, c.password) - resp, err := c.doRequest(req) if err != nil { @@ -150,30 +147,26 @@ func (c *Client) Get(getUrl string) (*http.Response, error) { switch resp.StatusCode { case http.StatusUnauthorized: - log.Debugf("Re-authorized with ticket url: %s", getUrl) - url, err := c.getAuthTicketURL(getUrl) - if err != nil { - return nil, err + log.Debugf("Re-auth required, performing CAS login again for: %s", getUrl) + if err := c.casLogin(); err != nil { + return nil, fmt.Errorf("re-auth failed: %s", err.Error()) } - req, err := http.NewRequest("GET", url, nil) - + req, err := http.NewRequest("GET", getUrl, nil) if err != nil { return nil, err } - req.SetBasicAuth(c.login, c.password) - return c.doRequest(req) case http.StatusBadRequest, http.StatusNotFound: - return nil, fmt.Errorf("respose CODE:%d ERR:%s", + return nil, fmt.Errorf("response CODE:%d ERR:%s", resp.StatusCode, readBodyMustString(resp.Body)) case http.StatusOK: return resp, nil default: - return resp, fmt.Errorf("unknown respose CODE: <%d>", resp.StatusCode) + return resp, fmt.Errorf("unknown response CODE: <%d>", resp.StatusCode) } } @@ -191,15 +184,6 @@ func (c *Client) doRequest(req *http.Request) (*http.Response, error) { } -func bodyToJSON(body io.ReadCloser, into interface{}) error { - b, err := readBody(body) - if err != nil { - return err - } - - return json.Unmarshal(b, into) -} - func readBody(body io.ReadCloser) ([]byte, error) { buf := get() defer put(buf) @@ -208,7 +192,9 @@ func readBody(body io.ReadCloser) ([]byte, error) { return nil, err } - return buf.Bytes(), err + result := make([]byte, buf.Len()) + copy(result, buf.Bytes()) + return result, nil } func readBodyMustString(body io.ReadCloser) string { @@ -221,6 +207,22 @@ func readBodyMustString(body io.ReadCloser) string { return string(buf) } +// checkResponseBody проверяет HTML-ответ на наличие страницы логина или ошибки сервера. +func checkResponseBody(body string) error { + if strings.Contains(body, "Личные данные") || + strings.Contains(body, `id="loginForm"`) { + return fmt.Errorf("сессия истекла, получена страница логина вместо данных") + } + if strings.Contains(body, "Ошибка на нашем сервере") || + strings.Contains(body, "сервис временно недоступен") { + return fmt.Errorf("сервер releases.1c.ru временно недоступен") + } + if strings.Contains(body, "Ошибка доступа") { + return fmt.Errorf("ошибка доступа на releases.1c.ru — нет прав на данный ресурс") + } + return nil +} + var pool = sync.Pool{ New: func() interface{} { return &bytes.Buffer{} diff --git a/downloader/html.go b/downloader/html.go index aa7fdec..2882a4b 100644 --- a/downloader/html.go +++ b/downloader/html.go @@ -165,7 +165,7 @@ func parseReleasesTable(s *goquery.Selection) (rows []ProjectInfo) { info.Name = strings.TrimSpace(releaseRow.Find(".nameColumn").Text()) info.Url, _ = releaseRow.Find(".nameColumn a").Attr("href") - info.ID = strings.TrimLeft(info.Url, "/project/") + info.ID = strings.TrimPrefix(info.Url, "/project/") releaseRow.Find("td").Each(func(i int, rowHtml *goquery.Selection) { diff --git a/downloader/oneDownloader.go b/downloader/oneDownloader.go index 37cb41e..4aeb297 100644 --- a/downloader/oneDownloader.go +++ b/downloader/oneDownloader.go @@ -1,6 +1,7 @@ package downloader import ( + "bytes" "fmt" "io" "net/http" @@ -86,22 +87,21 @@ func (dr *OnegetDownloader) Get(downloadConfigs ...DownloadConfig) ([]string, er mu := &sync.Mutex{} for fileToDownload := range downloadCh { go func(file *FileToDownload) { + defer func() { <-limit }() + defer dr.wg.Done() limit <- struct{}{} filename, err := dr.downloadFile(file) if err != nil { - dr.wg.Done() log.Errorf(err.Error()) + return } if len(filename) > 0 { mu.Lock() files = append(files, filename) mu.Unlock() } - dr.wg.Done() - - <-limit }(fileToDownload) @@ -154,8 +154,8 @@ func (dr *OnegetDownloader) getReleaseFiles(release *ProjectVersionInfo, config err := dr.addFileToChannel(file.url, config, downloadCh) if err != nil { - log.Errorf("Error get file from <%s>: %s", err.Error()) - multierr.Append(merr, err) + log.Errorf("Error get file: %s", err.Error()) + merr = multierr.Append(merr, err) } } @@ -166,16 +166,27 @@ func (dr *OnegetDownloader) getReleaseFiles(release *ProjectVersionInfo, config func (dr *OnegetDownloader) getProjectReleases(config DownloadConfig) ([]*ProjectVersionInfo, error) { - resp, err := dr.client.Get(projectHrefPrefix + config.Project) + resp, err := dr.client.Get(projectHrefPrefix + config.Project + projectHrefSuffix) if err != nil { return nil, err } defer resp.Body.Close() - releases, err := dr.parser.ParseProjectReleases(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error read project <%s> body: %s", config.Project, err.Error()) + } + + bodyStr := string(bodyBytes) + + if err := checkResponseBody(bodyStr); err != nil { + return nil, fmt.Errorf("project <%s>: %s", config.Project, err.Error()) + } + + releases, err := dr.parser.ParseProjectReleases(bytes.NewReader(bodyBytes)) if err != nil { return nil, fmt.Errorf("error parse project <%s> releases: %s, html: <%s>", - config.Project, err.Error(), readBodyMustString(resp.Body)) + config.Project, err.Error(), bodyStr) } return filterProjectVersionInfo(releases, config.Version), nil @@ -374,11 +385,12 @@ func SaveToFile(reader io.ReadCloser, fileName string) error { return err } - defer reader.Close() - defer fd.Close() + _, copyErr := io.Copy(fd, reader) + reader.Close() + fd.Close() - if _, err = io.Copy(fd, reader); err != nil && err != io.EOF { - return err + if copyErr != nil && copyErr != io.EOF { + return copyErr } return nil