package main import ( "crypto/sha1" "fmt" "io" "io/ioutil" "log" "net/http" "net/http/httputil" "net/url" "os" "sort" "strconv" "strings" "time" ) //apiV1Main version 1 main entry for all wechat callbacks // func apiV1Main(w http.ResponseWriter, r *http.Request) { logRequestDebug(httputil.DumpRequest(r, true)) if checkSignature(r) == false { log.Println("signature of URL incorrect") w.Header().Set("Content-Type", "text/xml; charset=utf-8") w.WriteHeader(http.StatusUnauthorized) fmt.Fprintf(w, "") //empty string return } switch r.Method { case "POST": //answerWechatPostEcho(w, r) answerWechatPost(w, r) case "GET": answerInitialAuth(w, r) default: log.Println(fmt.Sprintf("FATAL: Unhandled HTTP %s", r.Method)) response404Handler(w) //fmt.Fprintf(w, "Protocol Error: Expect GET or POST only") } } // //answerInitialAuth, when wechat first verify our URL for API hooks // func answerInitialAuth(w http.ResponseWriter, r *http.Request) { rq := r.URL.RawQuery m, _ := url.ParseQuery(rq) echostr, eok := m["echostr"] if checkSignature(r) && eok { fmt.Fprintf(w, echostr[0]) } else { fmt.Fprintf(w, "wtf is wrong with the Internet") } } // //answerWechatPost distribute PostRequest according to xml body info func answerWechatPost(w http.ResponseWriter, r *http.Request) { in, valid := readWechatInput(r) reply := "" //nothing w.Header().Set("Content-Type", "text/xml; charset=utf-8") if !valid { log.Println("Error: Invalid Input ") } else { //put into user session based pipeline AllInMessage <- &in //read instant response reply = <-in.instantResponse } in.destroy() //uncomment the followin two lines to enable echo test // state, _ := echoCommand(in.header.FromUserName, in) // reply = state.response fmt.Fprint(w, reply) } func readWechatInput(r *http.Request) (result InWechatMsg, valid bool) { body, err := ioutil.ReadAll(r.Body) if err != nil { log.Println(err) valid = false return } d := decryptToXML(string(body)) if d == "" { log.Println("Cannot decode Message : \r\n" + string(body)) valid = false return } valid = true fmt.Printf("decrypt as: %s\n", d) result.init() result.header = ReadCommonHeader(d) switch result.header.MsgType { case "text": result.body = ReadTextMsg(d) case "image": result.body = ReadPicMsg(d) case "voice": result.body = ReadVoiceMsg(d) case "video": result.body = ReadVideoMsg(d) case "shortvideo": result.body = ReadShortVideoMsg(d) case "location": result.body = ReadLocationMsg(d) case "link": result.body = ReadLinkMsg(d) case "event": result.body = ReadEventMsg(d) default: log.Println("Fatal: unknown incoming message type" + result.header.MsgType) valid = false } return } func answerWechatPostEcho(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) //fmt.Printf("get body: %s", string(body)) s := ReadEncryptedMsg(string(body)) //fmt.Printf("to decrypt %s", s.Encrypt) d := Decode(s.Encrypt) fmt.Printf("echo as: \r\n%s", d) e := strings.Replace(d, "ToUserName", "FDDD20170506xyzunique", 2) f := strings.Replace(e, "FromUserName", "ToUserName", 2) g := strings.Replace(f, "FDDD20170506xyzunique", "FromUserName", 2) fmt.Fprint(w, g) } // func checkSignature(r *http.Request) bool { return checkSignatureByToken(r, APIConfig.Token) } func checkSignatureByToken(r *http.Request, token string) bool { rq := r.URL.RawQuery m, _ := url.ParseQuery(rq) signature, sok := m["signature"] timestamp, tok := m["timestamp"] nonce, nok := m["nonce"] token = strings.TrimSpace(token) if sok && tok && nok && token != "" { return verifySignature(signature[0], timestamp[0], nonce[0], token) } return false } func checkCookieSignatureBytoken(r *http.Request, token string) bool { signature := "" nonce := "" timestamp := "" for _, c := range r.Cookies() { switch c.Name { case "signature": signature = c.Value case "nonce": nonce = c.Value case "timestamp": timestamp = c.Value } } if signature != "" && nonce != "" && timestamp != "" && token != "" { return verifySignature(signature, timestamp, nonce, IntraAPIConfig.CRMSecrete) } return false } func verifySignature(signature, timestamp, nonce, token string) bool { if timestampTooOldStr(timestamp) { return false } return signature == calculateSignature(timestamp, nonce, token) } func calculateSignature(timestamp, nonce, token string) (signature string) { //sort token, timestamp, nonce and join them strs := []string{token, timestamp, nonce} sort.Strings(strs) s := strings.Join(strs, "") signature = strSHA1(s) return } func strSHA1(s string) string { //calculate sha1 h := sha1.New() h.Write([]byte(s)) calculated := fmt.Sprintf("%x", h.Sum(nil)) return calculated } func timestampTooOldStr(timestamp string) bool { ts, err := strconv.Atoi(timestamp) if err != nil { return true } return timestampTooOld(int32(ts)) } func timestampTooOld(ts int32) bool { //diff > 3min from now now := int32(time.Now().Unix()) diff := now - ts if diff < 0 { diff = -diff } return diff > 180 //3 minutes, 180 seconds } func timestampOldThan(ts int32, sec int32) bool { //diff > 3min from now now := int32(time.Now().Unix()) diff := now - ts return diff > sec } func buildSignature(token string) (signature, timestamp, nonce string) { timestamp = fmt.Sprintf("%d", int32(time.Now().Unix())) nonce = RandStringRunes(10) // "1461107899" //a randome string cut from previous wechat request signature = calculateSignature(timestamp, nonce, token) return } func buildReqCommonSignature(req *http.Request, token string) { signature, timestamp, nonce := buildSignature(token) q := req.URL.Query() q.Add("signature", signature) q.Add("timestamp", timestamp) q.Add("nonce", nonce) req.URL.RawQuery = q.Encode() } func buildSignatureAppend2Url(oldURL string, token string) (newURL string) { u, err := url.Parse(oldURL) if err != nil { log.Println(err) newURL = oldURL return } signature, timestamp, nonce := buildSignature(token) q := u.Query() q.Add("signature", signature) q.Add("timestamp", timestamp) q.Add("nonce", nonce) u.RawQuery = q.Encode() newURL = u.String() return } // func checkSignature1() bool { // s1 := "e39de9f2e28079c01ebb4b803dfc3442b819545c" // t1 := "1492970761" // n1 := "1850971833" // token := APIConfig.Token // strs := []string{token, t1, n1} // sort.Strings(strs) // s := strings.Join(strs, "") // h := sha1.New() // h.Write([]byte(s)) // us := fmt.Sprintf("%x", h.Sum(nil)) // return s1 == us // } //webrootHandler sending contents to client when request "/" // essentially to prove the webserver is still alive // echo query string to the client func webrootHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) rq := r.URL.RawQuery m, _ := url.ParseQuery(rq) for index, element := range m { fmt.Fprintf(w, "\n%s => %s", index, element) } logRequestDebug(httputil.DumpRequest(r, true)) } func logRequestDebug(data []byte, err error) { if err == nil { fmt.Printf("%s\n\n", string(data)) } else { log.Fatalf("%s\n\n", err) } } //save uploaded 'file' into temporary location func uploadHandler(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(32 << 20) //32MB memory file, handler, err := r.FormFile("file") //form-field 'file' if err != nil { fmt.Println(err) return } defer file.Close() fmt.Fprintf(w, "%v", handler.Header) f, err := os.OpenFile("/tmp/wechat_hitxy_"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Println(err) return } defer f.Close() io.Copy(f, file) } //when user requst an attachment from our server, we get the file from CRM server and reply it back //in this way, user has no 'direct-access' to the CRM server. func crmAttachmentHandler(w http.ResponseWriter, r *http.Request) { fileID := getCrmFileID(r.URL.Path) saveAs := CRMConfig.AttachmentCache + "crm_attach_" + fileID //add prefix for easy deleting if isFileExist(saveAs) { http.ServeFile(w, r, saveAs) return } if fileID == "" { log.Println("invalid fileID") response404Handler(w) return } log.Printf("download %s => %s", fileID, saveAs) crmDownloadAttachmentAs(fileID, saveAs) if !isFileExist(saveAs) { response404Handler(w) return } http.ServeFile(w, r, saveAs) } func getCrmFileID(urlPath string) string { return strings.TrimPrefix(urlPath, "/crmfiles/") } func response404Handler(w http.ResponseWriter) { w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "not found") } func getHTTPRequestQuery(r *http.Request, name string) (value string) { rq := r.URL.RawQuery m, _ := url.ParseQuery(rq) v, ok := m[name] if ok { value = v[0] } return }