diff --git a/tools/authenticator/auth/auth.go b/tools/authenticator/auth/auth.go new file mode 100644 index 0000000..cdebfed --- /dev/null +++ b/tools/authenticator/auth/auth.go @@ -0,0 +1,432 @@ +package auth + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/url" + "regexp" + "strings" + + http "github.com/bogdanfinn/fhttp" + tls_client "github.com/bogdanfinn/tls-client" + pkce "github.com/nirasan/go-oauth-pkce-code-verifier" +) + +type Error struct { + Location string + StatusCode int + Details string + Error error +} + +func NewError(location string, statusCode int, details string, err error) *Error { + return &Error{ + Location: location, + StatusCode: statusCode, + Details: details, + Error: err, + } +} + +type Authenticator struct { + EmailAddress string + Password string + Proxy string + Session tls_client.HttpClient + UserAgent string + State string + URL string + Verifier_code string + Verifier_challenge string + AuthResult AuthResult +} + +type AuthResult struct { + AccessToken string `json:"access_token"` + PUID string `json:"puid"` +} + +func NewAuthenticator(emailAddress, password, proxy string) *Authenticator { + auth := &Authenticator{ + EmailAddress: emailAddress, + Password: password, + Proxy: proxy, + UserAgent: "ChatGPT/1.2023.187 (iOS 16.5.1; iPhone12,1; build 1744)", + } + jar := tls_client.NewCookieJar() + options := []tls_client.HttpClientOption{ + tls_client.WithTimeoutSeconds(20), + tls_client.WithClientProfile(tls_client.Safari_IOS_16_0), + tls_client.WithNotFollowRedirects(), + tls_client.WithCookieJar(jar), // create cookieJar instance and pass it as argument + // Proxy + tls_client.WithProxyUrl(proxy), + } + auth.Session, _ = tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) + + // PKCE + verifier, _ := pkce.CreateCodeVerifier() + auth.Verifier_code = verifier.String() + auth.Verifier_challenge = verifier.CodeChallengeS256() + + return auth +} + +func (auth *Authenticator) URLEncode(str string) string { + return url.QueryEscape(str) +} + +func (auth *Authenticator) Begin() *Error { + + url := "https://chat.openai.com/api/auth/csrf" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return NewError("begin", 0, "", err) + } + + req.Header.Set("Host", "chat.openai.com") + req.Header.Set("Accept", "*/*") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("User-Agent", auth.UserAgent) + req.Header.Set("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") + req.Header.Set("Referer", "https://chat.openai.com/auth/login") + req.Header.Set("Accept-Encoding", "gzip, deflate, br") + + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("begin", 0, "", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return NewError("begin", 0, "", err) + } + + if resp.StatusCode == 200 && strings.Contains(resp.Header.Get("Content-Type"), "json") { + + var csrfTokenResponse struct { + CsrfToken string `json:"csrfToken"` + } + err = json.Unmarshal(body, &csrfTokenResponse) + if err != nil { + return NewError("begin", 0, "", err) + } + + csrfToken := csrfTokenResponse.CsrfToken + return auth.partOne(csrfToken) + } else { + err := NewError("begin", resp.StatusCode, string(body), fmt.Errorf("error: Check details")) + return err + } +} + +func (auth *Authenticator) partOne(csrfToken string) *Error { + + auth_url := "https://chat.openai.com/api/auth/signin/auth0?prompt=login" + headers := map[string]string{ + "Host": "chat.openai.com", + "User-Agent": auth.UserAgent, + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "*/*", + "Sec-Gpc": "1", + "Accept-Language": "en-US,en;q=0.8", + "Origin": "https://chat.openai.com", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "Referer": "https://chat.openai.com/auth/login", + "Accept-Encoding": "gzip, deflate", + } + + // Construct payload + payload := fmt.Sprintf("callbackUrl=%%2F&csrfToken=%s&json=true", csrfToken) + req, _ := http.NewRequest("POST", auth_url, strings.NewReader(payload)) + + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("part_one", 0, "Failed to send request", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return NewError("part_one", 0, "Failed to read body", err) + } + + if resp.StatusCode == 200 && strings.Contains(resp.Header.Get("Content-Type"), "json") { + var urlResponse struct { + URL string `json:"url"` + } + err = json.Unmarshal(body, &urlResponse) + if err != nil { + return NewError("part_one", 0, "Failed to decode JSON", err) + } + if urlResponse.URL == "https://chat.openai.com/api/auth/error?error=OAuthSignin" || strings.Contains(urlResponse.URL, "error") { + err := NewError("part_one", resp.StatusCode, "You have been rate limited. Please try again later.", fmt.Errorf("error: Check details")) + return err + } + return auth.partTwo(urlResponse.URL) + } else { + return NewError("part_one", resp.StatusCode, string(body), fmt.Errorf("error: Check details")) + } +} + +func (auth *Authenticator) partTwo(url string) *Error { + + headers := map[string]string{ + "Host": "auth0.openai.com", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Connection": "keep-alive", + "User-Agent": auth.UserAgent, + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://ios.chat.openai.com/", + } + + req, _ := http.NewRequest("GET", url, nil) + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("part_two", 0, "Failed to make request", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode == 302 || resp.StatusCode == 200 { + + stateRegex := regexp.MustCompile(`state=(.*)`) + stateMatch := stateRegex.FindStringSubmatch(string(body)) + if len(stateMatch) < 2 { + return NewError("part_two", 0, "Could not find state in response", fmt.Errorf("error: Check details")) + } + + state := strings.Split(stateMatch[1], `"`)[0] + return auth.partThree(state) + } else { + return NewError("part_two", resp.StatusCode, string(body), fmt.Errorf("error: Check details")) + + } +} +func (auth *Authenticator) partThree(state string) *Error { + + url := fmt.Sprintf("https://auth0.openai.com/u/login/identifier?state=%s", state) + emailURLEncoded := auth.URLEncode(auth.EmailAddress) + + payload := fmt.Sprintf( + "state=%s&username=%s&js-available=false&webauthn-available=true&is-brave=false&webauthn-platform-available=true&action=default", + state, emailURLEncoded, + ) + + headers := map[string]string{ + "Host": "auth0.openai.com", + "Origin": "https://auth0.openai.com", + "Connection": "keep-alive", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "User-Agent": auth.UserAgent, + "Referer": fmt.Sprintf("https://auth0.openai.com/u/login/identifier?state=%s", state), + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/x-www-form-urlencoded", + } + + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) + + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("part_three", 0, "Failed to send request", err) + } + defer resp.Body.Close() + + if resp.StatusCode == 302 || resp.StatusCode == 200 { + return auth.partFour(state) + } else { + return NewError("part_three", resp.StatusCode, "Your email address is invalid.", fmt.Errorf("error: Check details")) + + } + +} +func (auth *Authenticator) partFour(state string) *Error { + + url := fmt.Sprintf("https://auth0.openai.com/u/login/password?state=%s", state) + emailURLEncoded := auth.URLEncode(auth.EmailAddress) + passwordURLEncoded := auth.URLEncode(auth.Password) + payload := fmt.Sprintf("state=%s&username=%s&password=%s&action=default", state, emailURLEncoded, passwordURLEncoded) + + headers := map[string]string{ + "Host": "auth0.openai.com", + "Origin": "https://auth0.openai.com", + "Connection": "keep-alive", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "User-Agent": auth.UserAgent, + "Referer": fmt.Sprintf("https://auth0.openai.com/u/login/password?state=%s", state), + "Accept-Language": "en-US,en;q=0.9", + "Content-Type": "application/x-www-form-urlencoded", + } + + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) + + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("part_four", 0, "Failed to send request", err) + } + defer resp.Body.Close() + + if resp.StatusCode == 302 { + redirectURL := resp.Header.Get("Location") + return auth.partFive(state, redirectURL) + } else { + body := bytes.NewBuffer(nil) + body.ReadFrom(resp.Body) + return NewError("part_four", resp.StatusCode, body.String(), fmt.Errorf("error: Check details")) + + } + +} +func (auth *Authenticator) partFive(oldState string, redirectURL string) *Error { + + url := "https://auth0.openai.com" + redirectURL + + headers := map[string]string{ + "Host": "auth0.openai.com", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Connection": "keep-alive", + "User-Agent": auth.UserAgent, + "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", + "Referer": fmt.Sprintf("https://auth0.openai.com/u/login/password?state=%s", oldState), + } + + req, _ := http.NewRequest("GET", url, nil) + + for k, v := range headers { + req.Header.Set(k, v) + } + + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("part_five", 0, "Failed to send request", err) + } + defer resp.Body.Close() + + if resp.StatusCode == 302 { + return auth.partSix(resp.Header.Get("Location"), url) + } else { + return NewError("part_five", resp.StatusCode, resp.Status, fmt.Errorf("error: Check details")) + + } + +} +func (auth *Authenticator) partSix(url, redirect_url string) *Error { + req, _ := http.NewRequest("GET", url, nil) + for k, v := range map[string]string{ + "Host": "chat.openai.com", + "Accept": "application/json", + "Connection": "keep-alive", + "User-Agent": auth.UserAgent, + "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", + "Referer": redirect_url, + } { + req.Header.Set(k, v) + } + resp, err := auth.Session.Do(req) + if err != nil { + return NewError("part_six", 0, "Failed to send request", err) + } + defer resp.Body.Close() + if err != nil { + return NewError("part_six", 0, "Response was not JSON", err) + } + if resp.StatusCode != 302 { + return NewError("part_six", resp.StatusCode, url, fmt.Errorf("incorrect response code")) + } + // Check location header + if location := resp.Header.Get("Location"); location != "https://chat.openai.com/" { + return NewError("part_six", resp.StatusCode, location, fmt.Errorf("incorrect redirect")) + } + + url = "https://chat.openai.com/api/auth/session" + + req, _ = http.NewRequest("GET", url, nil) + + // Set user agent + req.Header.Set("User-Agent", auth.UserAgent) + + resp, err = auth.Session.Do(req) + if err != nil { + return NewError("get_access_token", 0, "Failed to send request", err) + } + + if resp.StatusCode != 200 { + return NewError("get_access_token", resp.StatusCode, "Incorrect response code", fmt.Errorf("error: Check details")) + } + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return NewError("get_access_token", 0, "", err) + } + + // Check if access token in data + if _, ok := result["accessToken"]; !ok { + result_string := fmt.Sprintf("%v", result) + return NewError("part_six", 0, result_string, fmt.Errorf("missing access token")) + } + auth.AuthResult.AccessToken = result["accessToken"].(string) + + return nil +} + +func (auth *Authenticator) GetAccessToken() string { + return auth.AuthResult.AccessToken +} + +func (auth *Authenticator) GetPUID() (string, *Error) { + // Check if user has access token + if auth.AuthResult.AccessToken == "" { + return "", NewError("get_puid", 0, "Missing access token", fmt.Errorf("error: Check details")) + } + // Make request to https://chat.openai.com/backend-api/models + req, _ := http.NewRequest("GET", "https://chat.openai.com/backend-api/models", nil) + // Add headers + req.Header.Add("Authorization", "Bearer "+auth.AuthResult.AccessToken) + req.Header.Add("User-Agent", auth.UserAgent) + req.Header.Add("Accept", "application/json") + req.Header.Add("Accept-Language", "en-US,en;q=0.9") + req.Header.Add("Referer", "https://chat.openai.com/") + req.Header.Add("Origin", "https://chat.openai.com") + req.Header.Add("Connection", "keep-alive") + + resp, err := auth.Session.Do(req) + if err != nil { + return "", NewError("get_puid", 0, "Failed to make request", err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return "", NewError("get_puid", resp.StatusCode, "Failed to make request", fmt.Errorf("error: Check details")) + } + // Find `_puid` cookie in response + for _, cookie := range resp.Cookies() { + if cookie.Name == "_puid" { + auth.AuthResult.PUID = cookie.Value + return cookie.Value, nil + } + } + // If cookie not found, return error + return "", NewError("get_puid", 0, "PUID cookie not found", fmt.Errorf("error: Check details")) +} + +func (auth *Authenticator) GetAuthResult() AuthResult { + return auth.AuthResult +} diff --git a/tools/authenticator/go.mod b/tools/authenticator/go.mod index bd3ad8e..5bee707 100644 --- a/tools/authenticator/go.mod +++ b/tools/authenticator/go.mod @@ -2,16 +2,16 @@ module authenticator go 1.20 -require github.com/acheong08/OpenAIAuth v0.0.0-20230722134517-7f126fb9ee71 +require ( + github.com/bogdanfinn/fhttp v0.5.23 + github.com/bogdanfinn/tls-client v1.5.0 + github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c +) require ( github.com/andybalholm/brotli v1.0.5 // indirect - github.com/bogdanfinn/fhttp v0.5.23 // indirect - github.com/bogdanfinn/tls-client v1.5.0 // indirect github.com/bogdanfinn/utls v1.5.16 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/klauspost/compress v1.16.7 // indirect - github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c // indirect github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect golang.org/x/crypto v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect diff --git a/tools/authenticator/go.sum b/tools/authenticator/go.sum index 89d4731..ccdde73 100644 --- a/tools/authenticator/go.sum +++ b/tools/authenticator/go.sum @@ -1,44 +1,22 @@ -github.com/acheong08/OpenAIAuth v0.0.0-20230716132312-c206753c0819 h1:LE8tbbgfCcsUJZfQlpCF95eA+b2M6Q30ipzFjcpWp/I= -github.com/acheong08/OpenAIAuth v0.0.0-20230716132312-c206753c0819/go.mod h1:ES3Dh9hnbR2mDPlNTagj5e3b4nXECd4tbAjVgxggXEE= -github.com/acheong08/OpenAIAuth v0.0.0-20230716134840-dbf5ba4f9507 h1:1kZEmE1DeQEeIMHhQUqn+NqOdApF9BIv835ox09E2k8= -github.com/acheong08/OpenAIAuth v0.0.0-20230716134840-dbf5ba4f9507/go.mod h1:bkiXtklBFVpWHyWTys6Zhqb521i/gtT8cIUKWVx2m/M= -github.com/acheong08/OpenAIAuth v0.0.0-20230722134517-7f126fb9ee71 h1:IXULXNvaqA3gI6MDGAz/nW5snTC0iOqpm7pELmcyWyc= -github.com/acheong08/OpenAIAuth v0.0.0-20230722134517-7f126fb9ee71/go.mod h1:bkiXtklBFVpWHyWTys6Zhqb521i/gtT8cIUKWVx2m/M= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/bogdanfinn/fhttp v0.5.22 h1:U1jhZRtuaOanWWcm1WdMFnwMvSxUQgvO6berqAVTc5o= -github.com/bogdanfinn/fhttp v0.5.22/go.mod h1:brqi5woc5eSCVHdKYBV8aZLbO7HGqpwyDLeXW+fT18I= github.com/bogdanfinn/fhttp v0.5.23 h1:4Xb5OjYArB8GpnUw4A4r5jmt8UW0/Cvey3R9nS2dC9U= github.com/bogdanfinn/fhttp v0.5.23/go.mod h1:brqi5woc5eSCVHdKYBV8aZLbO7HGqpwyDLeXW+fT18I= -github.com/bogdanfinn/tls-client v1.3.12 h1:jpNj7owMY/oULUQyAhAv6tRFkliFGLyr8Qx1ZZY/gp8= -github.com/bogdanfinn/tls-client v1.3.12/go.mod h1:Q46nwIm0wPCweDM3XZcupxEIsTOWo3HVYSSsDj02/Qo= github.com/bogdanfinn/tls-client v1.5.0 h1:L/d4AL+8RRw+6o9wwhpii45I6RO2CEkgRV4NeE0NAxo= github.com/bogdanfinn/tls-client v1.5.0/go.mod h1:lgtqsHjoJYQMPz6H08bc8t30bmUaYnVjwtfVEzMGJDs= github.com/bogdanfinn/utls v1.5.16 h1:NhhWkegEcYETBMj9nvgO4lwvc6NcLH+znrXzO3gnw4M= github.com/bogdanfinn/utls v1.5.16/go.mod h1:mHeRCi69cUiEyVBkKONB1cAbLjRcZnlJbGzttmiuK4o= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= -github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c h1:4RYnE0ISVwRxm9Dfo7utw1dh0kdRDEmVYq2MFVLy5zI= github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c/go.mod h1:DvuJJ/w1Y59rG8UTDxsMk5U+UJXJwuvUgbiJSm9yhX8= github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/tools/authenticator/main.go b/tools/authenticator/main.go index 8634379..0fc081f 100644 --- a/tools/authenticator/main.go +++ b/tools/authenticator/main.go @@ -8,7 +8,7 @@ import ( "strings" "time" - auth "github.com/acheong08/OpenAIAuth/auth" + auth "authenticator/auth" ) type Account struct { @@ -127,8 +127,7 @@ func main() { println("Details: " + err.Details) println("Embedded error: " + err.Error.Error()) // Sleep for 10 seconds - time.Sleep(10 * time.Second) - continue + panic(err) } access_token := authenticator.GetAccessToken() // Append access token to access_tokens.txt diff --git a/tools/authenticator/tojson.sh b/tools/authenticator/tojson.sh new file mode 100755 index 0000000..e0f7863 --- /dev/null +++ b/tools/authenticator/tojson.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Check if a file name is provided as an argument +if [ $# -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +file="$1" +output="$2" +# Declare an empty array +lines=() + +# Read the file line by line and add each line to the array +while IFS= read -r line; do + lines+=("\"$line\"") +done < "$file" + +# Join array elements with commas and print the result enclosed in square brackets +result="[" +for ((i = 0; i < ${#lines[@]}; i++)); do + result+="${lines[i]}" + if ((i < ${#lines[@]} - 1)); then + result+="," + fi +done +result+="]" +if [ $# -eq 1 ]; then + echo "$result" +fi +if [ $# -eq 2 ]; then + echo "$result" | tee $output +fi +