github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/protocols/http/utils.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "compress/zlib" 7 "io" 8 "net/http" 9 "net/http/httputil" 10 "strings" 11 12 "github.com/pkg/errors" 13 "golang.org/x/text/encoding/simplifiedchinese" 14 "golang.org/x/text/transform" 15 16 "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" 17 "github.com/projectdiscovery/nuclei/v2/pkg/types" 18 "github.com/projectdiscovery/rawhttp" 19 mapsutil "github.com/projectdiscovery/utils/maps" 20 stringsutil "github.com/projectdiscovery/utils/strings" 21 ) 22 23 type redirectedResponse struct { 24 headers []byte 25 body []byte 26 fullResponse []byte 27 resp *http.Response 28 } 29 30 // dumpResponseWithRedirectChain dumps a http response with the 31 // complete http redirect chain. 32 // 33 // It preserves the order in which responses were given to requests 34 // and returns the data to the user for matching and viewing in that order. 35 // 36 // Inspired from - https://github.com/ffuf/ffuf/issues/324#issuecomment-719858923 37 func dumpResponseWithRedirectChain(resp *http.Response, body []byte) ([]redirectedResponse, error) { 38 var response []redirectedResponse 39 40 respData, err := httputil.DumpResponse(resp, false) 41 if err != nil { 42 return nil, err 43 } 44 respObj := redirectedResponse{ 45 headers: respData, 46 body: body, 47 resp: resp, 48 fullResponse: bytes.Join([][]byte{respData, body}, []byte{}), 49 } 50 if err := normalizeResponseBody(resp, &respObj); err != nil { 51 return nil, err 52 } 53 response = append(response, respObj) 54 55 var redirectResp *http.Response 56 if resp != nil && resp.Request != nil { 57 redirectResp = resp.Request.Response 58 } 59 for redirectResp != nil { 60 var body []byte 61 62 respData, err := httputil.DumpResponse(redirectResp, false) 63 if err != nil { 64 break 65 } 66 if redirectResp.Body != nil { 67 body, _ = io.ReadAll(redirectResp.Body) 68 } 69 respObj := redirectedResponse{ 70 headers: respData, 71 body: body, 72 resp: redirectResp, 73 fullResponse: bytes.Join([][]byte{respData, body}, []byte{}), 74 } 75 if err := normalizeResponseBody(redirectResp, &respObj); err != nil { 76 return nil, err 77 } 78 response = append(response, respObj) 79 redirectResp = redirectResp.Request.Response 80 } 81 return response, nil 82 } 83 84 // normalizeResponseBody performs normalization on the http response object. 85 func normalizeResponseBody(resp *http.Response, response *redirectedResponse) error { 86 var err error 87 // net/http doesn't automatically decompress the response body if an 88 // encoding has been specified by the user in the request so in case we have to 89 // manually do it. 90 dataOrig := response.body 91 response.body, err = handleDecompression(resp, response.body) 92 // in case of error use original data 93 if err != nil { 94 response.body = dataOrig 95 } 96 response.fullResponse = bytes.ReplaceAll(response.fullResponse, dataOrig, response.body) 97 98 // Decode gbk response content-types 99 // gb18030 supersedes gb2312 100 responseContentType := resp.Header.Get("Content-Type") 101 if isContentTypeGbk(responseContentType) { 102 response.fullResponse, err = decodeGBK(response.fullResponse) 103 if err != nil { 104 return errors.Wrap(err, "could not gbk decode") 105 } 106 107 // the uncompressed body needs to be decoded to standard utf8 108 response.body, err = decodeGBK(response.body) 109 if err != nil { 110 return errors.Wrap(err, "could not gbk decode") 111 } 112 } 113 return nil 114 } 115 116 // dump creates a dump of the http request in form of a byte slice 117 func dump(req *generatedRequest, reqURL string) ([]byte, error) { 118 if req.request != nil { 119 return req.request.Dump() 120 } 121 rawHttpOptions := &rawhttp.Options{CustomHeaders: req.rawRequest.UnsafeHeaders, CustomRawBytes: req.rawRequest.UnsafeRawBytes} 122 return rawhttp.DumpRequestRaw(req.rawRequest.Method, reqURL, req.rawRequest.Path, generators.ExpandMapValues(req.rawRequest.Headers), io.NopCloser(strings.NewReader(req.rawRequest.Data)), rawHttpOptions) 123 } 124 125 // handleDecompression if the user specified a custom encoding (as golang transport doesn't do this automatically) 126 func handleDecompression(resp *http.Response, bodyOrig []byte) (bodyDec []byte, err error) { 127 if resp == nil { 128 return bodyOrig, nil 129 } 130 131 var reader io.ReadCloser 132 switch resp.Header.Get("Content-Encoding") { 133 case "gzip": 134 reader, err = gzip.NewReader(bytes.NewReader(bodyOrig)) 135 case "deflate": 136 reader, err = zlib.NewReader(bytes.NewReader(bodyOrig)) 137 default: 138 return bodyOrig, nil 139 } 140 if err != nil { 141 return nil, err 142 } 143 defer reader.Close() 144 145 bodyDec, err = io.ReadAll(reader) 146 if err != nil { 147 return bodyOrig, err 148 } 149 return bodyDec, nil 150 } 151 152 // decodeGBK converts GBK to UTF-8 153 func decodeGBK(s []byte) ([]byte, error) { 154 I := bytes.NewReader(s) 155 O := transform.NewReader(I, simplifiedchinese.GBK.NewDecoder()) 156 d, e := io.ReadAll(O) 157 if e != nil { 158 return nil, e 159 } 160 return d, nil 161 } 162 163 // isContentTypeGbk checks if the content-type header is gbk 164 func isContentTypeGbk(contentType string) bool { 165 contentType = strings.ToLower(contentType) 166 return stringsutil.ContainsAny(contentType, "gbk", "gb2312", "gb18030") 167 } 168 169 // if template contains more than 1 request and matchers require requestcondition from 170 // both requests , then we need to request for event from interactsh even if current request 171 // doesnot use interactsh url in it 172 func getInteractshURLsFromEvent(event map[string]interface{}) []string { 173 interactshUrls := map[string]struct{}{} 174 for k, v := range event { 175 if strings.HasPrefix(k, "interactsh-url") { 176 interactshUrls[types.ToString(v)] = struct{}{} 177 } 178 } 179 return mapsutil.GetKeys(interactshUrls) 180 }