I am trying to send a GET request to https://nseindia.com/api/historical/cm/equity?from=01-01-2023&series=%5B%22EQ%22%5D&symbol=PAYTM&to=24-01-2023. It works fine if I run it locally, on VS Code's Thunder Client, Postman on web etc, i.e. the request is working fine. The code is also working perfectly. But when I construct a lambda function for it on AWS, it gives and error with Content-Type http/text whereas it should be giving gzip encoding. This is the error it is giving:
You don't have permission to access "http://www.nseindia.com/api/historical/cm/equity" on this server. Reference #18.9832d417.1675774874.1bc46bd8
So basically the issue is that AWS Lambda is being denied access to the resources of the destination server. My question being, is there any alternative method on lambda to make this request work? Do I need to add some proxy? Do I need any workaround?
This is the Postman link if anyone wants to cross-check the req. First run Initial cookie fetch and use the cookies in response for the next request: Historical data which actually fetches the data from the intended server
I am posting the minimal code after cutting out the jargon (working perfectly fine), if anyone wants to try it on their own.
package main
import (
"compress/gzip"
"encoding/json"
"errors"
"io"
"net/http"
"time"
"github.com/aws/aws-lambda-go/lambda"
)
type DailyDataType struct {
ID string `json:"_id"`
Symbol string `json:"CH_SYMBOL"`
Series string `json:"CH_SERIES"`
MarkeyType string `json:"CH_MARKET_TYPE"`
High float32 `json:"CH_TRADE_HIGH_PRICE"`
Low float32 `json:"CH_TRADE_LOW_PRICE"`
Open float32 `json:"CH_OPENING_PRICE"`
Close float32 `json:"CH_CLOSING_PRICE"`
LTP float32 `json:"CH_LAST_TRADED_PRICE"`
PrevClose float32 `json:"CH_PREVIOUS_CLS_PRICE"`
TotalTradedQuantity int32 `json:"CH_TOT_TRADED_QTY"`
TotalTradedValue float64 `json:"CH_TOT_TRADED_VAL"`
High52W float32 `json:"CH_52WEEK_HIGH_PRICE"`
Low52W float32 `json:"CH_52WEEK_LOW_PRICE"`
TotalTrades int32 `json:"CH_TOTAL_TRADES"`
ISIN string `json:"CH_ISIN"`
ChTimestamp string `json:"CH_TIMESTAMP"`
Timestamp string `json:"TIMESTAMP"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
V int8 `json:"__v"`
VWAP float32 `json:"VWAP"`
MTimestamp string `json:"mTIMESTAMP"`
}
type SecurityDataType struct {
Meta interface{} `json:"meta"`
Data []DailyDataType `json:"data"`
}
var BaseURL string = "https://www.nseindia.com"
func ReqConfig() *http.Request {
req, _ := http.NewRequest("GET", BaseURL, nil)
req.Header.Add("Accept", "*/*")
req.Header.Add("Accept-Encoding", "gzip, deflate, br")
req.Header.Add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
req.Header.Add("Connection", "keep-alive")
req.Header.Add("Host", "www.nseindia.com")
req.Header.Add("Referer", "https://www.nseindia.com/get-quotes/equity")
req.Header.Add("X-Requested-With", "XMLHttpRequest")
req.Header.Add("sec-fetch-dest", "empty")
req.Header.Add("sec-fetch-mode", "cors")
req.Header.Add("pragma", "no-cache")
req.Header.Add("sec-fetch-site", "same-origin")
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
for _, cookie := range res.Cookies() {
req.AddCookie(cookie)
}
cookies := req.Cookies()
for i := 0; i < len(cookies); i++ {
for j := i + 1; j < len(cookies); j++ {
if cookies[i].Name == cookies[j].Name {
cookies = append(cookies[:j], cookies[j+1:]...)
j--
}
}
}
req.Header.Del("Cookie")
for _, cookie := range cookies {
req.AddCookie(cookie)
}
return req
}
func FetchHistoricalData(symbol string, from string, to string, series string) (SecurityDataType, error) {
req := ReqConfig()
query := req.URL.Query()
query.Add("symbol", symbol)
query.Add("from", from)
query.Add("to", to)
query.Add("series", "[\""+series+"\"]")
req.URL.RawQuery = query.Encode()
req.URL.Path = "/api/historical/cm/equity"
client := &http.Client{Timeout: 40 * time.Second}
res, err := client.Do(req)
defer res.Body.Close()
var securityData SecurityDataType
if err != nil {
return securityData, err
}
switch res.Header.Get("Content-Encoding") {
case "gzip":
var gzipReader io.ReadCloser
gzipReader, _ = gzip.NewReader(res.Body)
err = json.NewDecoder(gzipReader).Decode(&securityData)
defer gzipReader.Close()
case "application/json":
var results map[string]interface{}
err = json.NewDecoder(res.Body).Decode(&results)
default:
bodyBytes, _ := io.ReadAll(res.Body)
bodyString := string(bodyBytes)
return securityData, errors.New(bodyString)
}
if err != nil {
return securityData, err
}
return securityData, err
}
func HandleRequest() {
dateToday := time.Now()
dateYesterday := time.Now().AddDate(0, 0, -1)
data, err := FetchHistoricalData("JSL", dateYesterday.Format("02-01-2006"), dateToday.Format("02-01-2006"), "EQ")
}
func main() {
lambda.Start(HandleRequest)
// HandleRequest()
}
Related
I wrote a lambda function in Go that would handle a form submission and send an email with the captured form data to a configured email address.
I am using AWS SES, or trying to.
My emailService.go has the following code:
package aj
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"mime/multipart"
"net/http"
"net/smtp"
"os"
"path/filepath"
"strings"
"go.uber.org/zap"
)
var (
host = os.Getenv("EMAIL_HOST")
username = os.Getenv("EMAIL_USERNAME")
password = os.Getenv("EMAIL_PASSWORD")
portNumber = os.Getenv("EMAIL_PORT")
from = os.Getenv("SOURCE_EMAIL")
)
type EmailService struct {
auth smtp.Auth
}
type EmailSender interface {
SendEmail(logger *zap.Logger, ctx context.Context, m *Message) error
}
func NewEmailService() *EmailService {
auth := smtp.PlainAuth("", username, password, host)
return &EmailService{auth}
}
func (s *EmailService) SendEmail(logger *zap.Logger, ctx context.Context, m *Message) error {
return smtp.SendMail(fmt.Sprintf("%s:%s", host, portNumber), s.auth, from, m.To, m.ToBytes())
}
func NewMessage(s, b string) *Message {
return &Message{Subject: s, Body: b, Attachments: make(map[string][]byte)}
}
func (m *Message) AttachFile(src string) error {
b, err := ioutil.ReadFile(src)
if err != nil {
return err
}
_, fileName := filepath.Split(src)
m.Attachments[fileName] = b
return nil
}
func (m *Message) ToBytes() []byte {
buf := bytes.NewBuffer(nil)
withAttachments := len(m.Attachments) > 0
buf.WriteString(fmt.Sprintf("Subject: %s\n", m.Subject))
buf.WriteString(fmt.Sprintf("To: %s\n", strings.Join(m.To, ",")))
if len(m.CC) > 0 {
buf.WriteString(fmt.Sprintf("Cc: %s\n", strings.Join(m.CC, ",")))
}
if len(m.BCC) > 0 {
buf.WriteString(fmt.Sprintf("Bcc: %s\n", strings.Join(m.BCC, ",")))
}
buf.WriteString("MIME-Version: 1.0\n")
writer := multipart.NewWriter(buf)
boundary := writer.Boundary()
if withAttachments {
buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\n", boundary))
buf.WriteString(fmt.Sprintf("--%s\n", boundary))
} else {
buf.WriteString("Content-Type: text/plain; charset=utf-8\n")
}
buf.WriteString(m.Body)
if withAttachments {
for k, v := range m.Attachments {
buf.WriteString(fmt.Sprintf("\n\n--%s\n", boundary))
buf.WriteString(fmt.Sprintf("Content-Type: %s\n", http.DetectContentType(v)))
buf.WriteString("Content-Transfer-Encoding: base64\n")
buf.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\n", k))
b := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
base64.StdEncoding.Encode(b, v)
buf.Write(b)
buf.WriteString(fmt.Sprintf("\n--%s", boundary))
}
buf.WriteString("--")
}
return buf.Bytes()
}
As you can see, I am using the standard library net/smtp package. Is that Ok to do when you want to send an email through SES? Of course, I am passing my SES SMTP credentials for the necessary/required environment variables.
Right now, I'm not getting any errors, but I am also not receiving emails even though the count on "emails sent" on the SES Dashboard (within the AWS Console) increments each time I send a request to my lambda function.
Am I going about this the wrong way? It is not working because I must use the SES package within the AWS SDK?
In the HTTP handler, I'm doing:
err = r.ParseMultipartForm(10 << 20)
if err != nil {
logger.Error("error parsing form data", zap.Error(err))
http.Error(w, "error parsing form data", http.StatusBadRequest)
return
}
I believe there's a limit of 10Mb on SES emails, and I believe the 10 << 20 ensure I don't go over? Correct me if I'm wrong please.
Here when I'm printing the activity it is printing them in the order they are getting created but at the time of assertion it is picking up activities in random order and also it is picking expected values in random order. The api that I'm calling mastercontroller have some goroutines and could take time maybe that is the reason but not sure.
for i, param := range params {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
URL: &url.URL{},
Header: make(http.Header),
}
MockJsonPost(ctx, param)
MasterController(ctx)
time.Sleep(3 * time.Second)
fmt.Println("response body", string(w.Body.Bytes()))
fmt.Println("status", w.Code)
// var activity *activity.Activity
activity, err := activityController.GetLastActivity(nil)
//tx.Raw("select * from activity order by id desc limit 1").Find(&activity)
if err != nil {
fmt.Println("No activity found")
}
activityJson, err := activity.ToJsonTest()
if err != nil {
fmt.Println("error converting in json")
}
fmt.Printf("reponse activity %+v", string(activityJson))
assert.EqualValues(t, string(expected[i]), string(activityJson))
}
func MockJsonPost(c *gin.Context, content interface{}) {
c.Request.Method = "POST" // or PUT
c.Request.Header.Set("Content-Type", "application/json")
jsonbytes, err := json.Marshal(content)
if err != nil {
panic(err)
}
// the request body must be an io.ReadCloser
// the bytes buffer though doesn't implement io.Closer,
// so you wrap it in a no-op closer
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
}
I'm currently trying to send a POST request to an external API from a GCP Cloud Function. I've tested the function extensively locally and it fulfills the request every time and also works from Postman, but when I run the exact same code from within a cloud function, it returns a 500 from the external API every single time.
I'm genuinely at a loss as to why when sending the POST request from within the cloud function it fails every single time.
Does GCP add any headers that might interfere with an external API call or is there a configuration option within the cloud function settings that needs to be configured to allow an external POST request?
I've attempted to implement an http retry mechanism, but that did not work either.
Again, locally and from Postman, the exact same code is successful every time I run it.
Here is the code I use to generate and send the request:
package email
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/hashicorp/go-retryablehttp"
)
var FailedRequestErr = errors.New("failed request to moosend")
const (
successCode = 0
moosendHost = "api.moosend.com/v3"
dailyNewsletterMailingListID = "2e461f4c-99d1-4a8e-80ea-168b20bdaf5f"
mainEmail = "jason#functionalbits.io"
campaignNameBase = "Functional Bits Newsletter - Issue"
campaignSubjectBase = "Functional Bits Issue"
)
type CreatingADraftCampaignRequest struct {
Name string `json:"Name"`
Subject string `json:"Subject"`
SenderEmail string `json:"SenderEmail"`
ReplyToEmail string `json:"ReplyToEmail"`
IsAB string `json:"IsAB"`
ConfirmationToEmail string `json:"ConfirmationToEmail,omitempty"`
WebLocation string `json:"WebLocation,omitempty"`
MailingLists []MailingLists `json:"MailingLists,omitempty"`
SegmentID string `json:"SegmentID,omitempty"`
ABCampaignType string `json:"ABCampaignType,omitempty"`
TrackInGoogleAnalytics string `json:"TrackInGoogleAnalytics,omitempty"`
DontTrackLinkClicks string `json:"DontTrackLinkClicks,omitempty"`
SubjectB string `json:"SubjectB,omitempty"`
WebLocationB string `json:"WebLocationB,omitempty"`
SenderEmailB string `json:"SenderEmailB,omitempty"`
HoursToTest string `json:"HoursToTest,omitempty"`
ListPercentage string `json:"ListPercentage,omitempty"`
ABWinnerSelectionType string `json:"ABWinnerSelectionType,omitempty"`
}
type MailingLists struct {
MailingListID string `json:"MailingListId"`
SegmentID float64 `json:"SegmentId,omitempty"`
}
type CampaignResponse struct {
Code int32 `json:"Code"`
Err interface{} `json:"Error"`
Context interface{} `json:"Context"`
}
type MoosendAPI struct {
apiKey string
client *http.Client
}
func NewMoosendAPI(apiKey string) *MoosendAPI {
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 5
standardClient := retryClient.StandardClient()
return &MoosendAPI{
apiKey: apiKey,
client: standardClient,
}
}
func (m *MoosendAPI) CreateDraftCampaign(issueNumber string, webLocation string) (*CampaignResponse, error) {
campaign := CreatingADraftCampaignRequest{
Name: fmt.Sprintf("%s %s", campaignNameBase, issueNumber),
Subject: fmt.Sprintf("%s %s", campaignSubjectBase, issueNumber),
IsAB: "false",
WebLocation: webLocation,
MailingLists: []MailingLists{{MailingListID: dailyNewsletterMailingListID}},
SenderEmail: mainEmail,
ReplyToEmail: mainEmail,
ConfirmationToEmail: mainEmail,
TrackInGoogleAnalytics: "true",
}
body, err := json.Marshal(&campaign)
if err != nil {
log.Println("error marshalling campaign request")
return nil, err
}
fullURL := fmt.Sprintf("https://%s/campaigns/create.json?apikey=%s", moosendHost, m.apiKey)
req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewBuffer(body))
if err != nil {
log.Println("request error")
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
log.Printf("request: %+v", req)
resp, err := m.client.Do(req)
if resp.StatusCode != http.StatusOK {
return nil, FailedRequestErr
}
if err != nil {
log.Println("error sending request")
return nil, err
}
log.Printf("response: %+v", resp)
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("error reading response body")
return nil, err
}
var draftResponse CampaignResponse
if err := json.Unmarshal(respBody, &draftResponse); err != nil {
log.Println("error unmarshalling response")
log.Printf("%+v", draftResponse)
return nil, err
}
return &draftResponse, nil
}
func (m *MoosendAPI) SendCampaign(campaignID string) error {
fullURL := fmt.Sprintf("https://%s/campaigns/%s/send.json?apikey=%s", moosendHost, campaignID, m.apiKey)
req, err := http.NewRequest(http.MethodPost, fullURL, nil)
if err != nil {
log.Println("error creating request")
return err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
resp, err := m.client.Do(req)
if err != nil {
log.Println("error sending request")
return err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("error reading response body")
return err
}
var sendResponse CampaignResponse
if err := json.Unmarshal(respBody, &sendResponse); err != nil {
log.Println("error unmarshalling response")
log.Printf("%+v", sendResponse)
return err
}
return nil
}
Then how it's run in the main function code:
package function
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"github.com/Functional-Bits/emailer-service/internal/email"
"github.com/Functional-Bits/emailer-service/internal/publish"
)
func CampaignGenerator(ctx context.Context, m publish.PubSubMessage) error {
moosendAPIKey, ok := os.LookupEnv("MOOSEND_API_KEY")
if !ok {
log.Println("missing moosendAPIKey")
}
mAPI := email.NewMoosendAPI(moosendAPIKey)
var msg publish.IncomingMessage
if err := json.Unmarshal(m.Data, &msg); err != nil {
log.Println(err)
return err
}
log.Printf("received message: %+v", msg)
log.Printf("generating draft campaign for issue %s", msg.IssueNumber)
draftResponse, err := mAPI.CreateDraftCampaign(msg.IssueNumber, msg.FileURL)
if err != nil {
log.Println(err)
return err
}
log.Printf("draft response: %+v", draftResponse)
campaignID, ok := draftResponse.Context.(string)
if !ok {
log.Printf("response didn't contain an ID: %+v", draftResponse)
return errors.New("no campaign generated")
}
log.Printf("sending campgain %s", campaignID)
if err := mAPI.SendCampaign(campaignID); err != nil {
log.Println(err)
return err
}
log.Printf("campaign successfully sent for issue number %s", msg.IssueNumber)
return nil
}
When this code is run locally, It correctly makes the 2 calls and sends an email campaign. When run from the cloud function I get a 500 internal server error with no additional information as to why. Link to API docs.
I get the following response from the external API (from my cloud function logs)
response: &{
Status:500 Internal Server Error
StatusCode:500
Proto:HTTP/1.1
ProtoMajor:1
ProtoMinor:1
Header:map[Access-Control-Allow-Headers:[Content-Type, Accept, Cache-Control, X-Requested-With]
Access-Control-Allow-Methods:[GET, POST, OPTIONS, DELETE, PUT]
Access-Control-Allow-Origin:[*]
Cache-Control:[private]
Content-Length:[12750]
Content-Type:[text/html; charset=utf-8]
Date:[Sun, 12 Dec 2021 07:00:09 GMT]
Server:[Microsoft-IIS/10.0]
X-Aspnet-Version:[4.0.30319]
X-Powered-By:[ASP.NET]
X-Robots-Tag:[noindex, nofollow]
X-Server-Id:[1]]
Body:0xc0003f04c0
ContentLength:12750
TransferEncoding:[]
Close:false
Uncompressed:false
Trailer:map[]
Request:0xc000160b00
TLS:0xc000500630
}
The response causes an unmarshal error because no campaign ID is returned.
I'm not in charge of maintaining the code base under test, but I have this method to try to test, which hits a real server:
func (a *APICoreSt) CallServer(context interface{}, apiType APIType, apiURL string, header map[string]string, jsonBody []byte) CallResultSt {
var (
Url = a.BaseURL + apiURL
err error
res *http.Response
resBody json.RawMessage
hc = &http.Client{}
req = new(http.Request)
)
req, err = http.NewRequest(string(apiType), Url, bytes.NewBuffer(jsonBody))
if err != nil {
//Use a map instead of errorSt so that it doesn't create a heavy dependency.
errorSt := map[string]string{
"Code": "ez020300007",
"Message": "The request failed to be created.",
}
logger.Instance.LogError(err.Error())
err, _ := json.Marshal(errorSt)
return CallResultSt{DetailObject: err, IsSucceeded: false}
}
for k, v := range header {
req.Header.Set(k, v)
}
res, err = hc.Do(req)
if res != nil {
resBody, err = ioutil.ReadAll(res.Body)
res.Body = ioutil.NopCloser(bytes.NewBuffer(resBody))
}
return CallResultSt{resBody, logger.Instance.CheckAndHandleErr(context, res)}
}
Clearly, since this is a unit test, I don't want to be making this call for real, and the test depends on external factors. Is there any way to inject any fakes/mocks, without touching this func itself (it's literally not my job to touch it)?
I try to use golang to login in a private area of a website and pull some info, but i don't quite seem to get it right.
I manage to fetch the login page to get the csrf token, then i post the csrf token together with the login info to the login page and i login just fine. If i stop at this point, i can see the page where i am redirected. However, any subsequent calls from this point on will redirect me back to login.
The code
package main
import (
"github.com/PuerkitoBio/goquery"
"io"
_ "io/ioutil"
"log"
"net/http"
"net/url"
_ "strings"
"sync"
)
type Jar struct {
sync.Mutex
cookies map[string][]*http.Cookie
}
func NewJar() *Jar {
jar := new(Jar)
jar.cookies = make(map[string][]*http.Cookie)
return jar
}
func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
jar.Lock()
jar.cookies[u.Host] = cookies
jar.Unlock()
}
func (jar *Jar) Cookies(u *url.URL) []*http.Cookie {
return jar.cookies[u.Host]
}
func NewJarClient() *http.Client {
return &http.Client{
Jar: NewJar(),
}
}
func fetch(w http.ResponseWriter, r *http.Request) {
// create the client
client := NewJarClient()
// get the csrf token
req, _ := http.NewRequest("GET", "http://www.domain.com/login", nil)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
doc, err := goquery.NewDocumentFromResponse(resp)
if err != nil {
log.Fatal(err)
}
csrfToken := ""
if val, ok := doc.Find(`head meta[name="csrf-token-value"]`).Attr("content"); ok {
csrfToken = val
}
// post on the login form.
resp, _ = client.PostForm("http://www.domain.com/login", url.Values{
"UserLogin[email]": {"the email"},
"UserLogin[password]": {"the password"},
"csrf_token": {csrfToken},
})
doc, err = goquery.NewDocumentFromResponse(resp)
if err != nil {
log.Fatal(err)
}
// if i stop here then i can see just fine the dashboard where i am redirected after login.
// but if i continue and request a 3rd page, then i get the login page again,
// sign that i lose the cookies and i am redirected back
// html, _ := doc.Html()
// io.WriteString(w, html)
// return
// from this point on, any request will give me the login page once again.
// i am not sure why since the cookies should be set and sent on all requests
req, _ = http.NewRequest("GET", "http://www.domain.com/dashboard", nil)
resp, err = client.Do(req)
if err != nil {
log.Fatal(err)
}
doc, err = goquery.NewDocumentFromResponse(resp)
if err != nil {
log.Fatal(err)
}
html, _ := doc.Html()
io.WriteString(w, html)
}
func main() {
http.HandleFunc("/", fetch)
http.ListenAndServe("127.0.0.1:49721", nil)
}
Any idea what i am missing here ?
Ok, the issue is the cookie jar implementation, more specifically the SetCookies function, which right now is:
func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
jar.Lock()
jar.cookies[u.Host] = cookies
jar.Unlock()
}
And this is wrong because new cookies instead of being added to the existing ones they will simply be added as new discarding the old ones.
It seems the right way to do this is:
func (jar *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) {
jar.Lock()
if _, ok := jar.cookies[u.Host]; ok {
for _, c := range cookies {
jar.cookies[u.Host] = append(jar.cookies[u.Host], c)
}
} else {
jar.cookies[u.Host] = cookies
}
jar.Unlock()
}