package web import ( "bytes" "errors" "fmt" "net/http" "strconv" "strings" "time" "pcap-web-tools/internal/hexutil" "pcap-web-tools/internal/inet" "pcap-web-tools/internal/pcap" ) const maxFormBytes = 2 << 20 type linkTypeOption struct { Value string Label string } var linkTypeOptions = []linkTypeOption{ {"auto", "Auto (Ethernet, or Raw IP when layer 2 is empty and layer 3 is set)"}, {"1", "Ethernet (1)"}, {"101", "Raw IP (101)"}, {"113", "Linux cooked SLL (113)"}, {"276", "Linux cooked SLL2 (276)"}, {"105", "IEEE 802.11 (105)"}, {"custom", "Custom…"}, } type hex2pcapForm struct { L2, L3, L7 string Fix bool LinkType string CustomLinkType string Filename string } var hex2pcapExample = hex2pcapForm{ L2: "00 11 22 33 44 55 66 77 88 99 aa bb 08 00", L3: "45 00 00 00 00 01 40 00 40 11 00 00 c0 a8 00 01 c0 a8 00 02\n30 39 04 d2 00 00 00 00", L7: "68 65 6c 6c 6f 2c 20 70 63 61 70 21", Fix: true, LinkType: "auto", } type hex2pcapResult struct { Packet []byte LinkType uint32 LinkTypeName string L2Len int L3Len int L7Len int Fixed bool IPOffset int Fix inet.Result Dump []dumpRow } type hex2pcapPage struct { Form hex2pcapForm Options []linkTypeOption Error string Result *hex2pcapResult Filename string } func (s *Server) handleHex2PcapForm(w http.ResponseWriter, r *http.Request) { form := hex2pcapForm{Fix: true, LinkType: "auto"} if r.URL.Query().Has("example") { form = hex2pcapExample } s.render(w, http.StatusOK, "hex2pcap.html", "/hex2pcap", hex2pcapPage{Form: form, Options: linkTypeOptions}) } func (s *Server) handleHex2Pcap(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes) if err := r.ParseForm(); err != nil { http.Error(w, "bad form: "+err.Error(), http.StatusBadRequest) return } form := hex2pcapForm{ L2: r.PostFormValue("l2"), L3: r.PostFormValue("l3"), L7: r.PostFormValue("l7"), Fix: r.PostFormValue("fix") != "", LinkType: r.PostFormValue("linktype"), CustomLinkType: r.PostFormValue("customlinktype"), Filename: r.PostFormValue("filename"), } page := hex2pcapPage{Form: form, Options: linkTypeOptions, Filename: pcapFilename(form.Filename)} res, err := buildPacket(form) if err != nil { page.Error = err.Error() s.render(w, http.StatusBadRequest, "hex2pcap.html", "/hex2pcap", page) return } if r.PostFormValue("action") != "download" { page.Result = res s.render(w, http.StatusOK, "hex2pcap.html", "/hex2pcap", page) return } var buf bytes.Buffer pw, err := pcap.NewWriter(&buf, res.LinkType) if err == nil { err = pw.WritePacket(time.Now(), res.Packet) } if err != nil { page.Error = err.Error() s.render(w, http.StatusBadRequest, "hex2pcap.html", "/hex2pcap", page) return } w.Header().Set("Content-Type", "application/vnd.tcpdump.pcap") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", page.Filename)) w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) w.Write(buf.Bytes()) } // buildPacket decodes and concatenates the hex fields, and optionally fixes // up the IP lengths and checksums. func buildPacket(f hex2pcapForm) (*hex2pcapResult, error) { var layers [3][]byte for i, in := range []struct{ name, hex string }{ {"Layer 2", f.L2}, {"Layer 3", f.L3}, {"Payload", f.L7}, } { b, err := hexutil.Decode(in.hex) if err != nil { return nil, fmt.Errorf("%s: %w", in.name, err) } layers[i] = b } l2, l3, l7 := layers[0], layers[1], layers[2] pkt := bytes.Join(layers[:], nil) if len(pkt) == 0 { return nil, errors.New("all fields are empty; paste some hex") } if len(pkt) > pcap.DefaultSnapLen { return nil, fmt.Errorf("packet is %d bytes; maximum is %d", len(pkt), pcap.DefaultSnapLen) } lt, err := resolveLinkType(f, len(l2) > 0, len(l3) > 0) if err != nil { return nil, err } res := &hex2pcapResult{ LinkType: lt, LinkTypeName: pcap.LinkTypeName(lt), L2Len: len(l2), L3Len: len(l3), L7Len: len(l7), } orig := bytes.Clone(pkt) if f.Fix { // The layer 3 field marks where the headers end, which tells Fix the // TCP header length and which SCTP chunk the payload belongs to. ipOff, hdrLen := len(l2), len(l3) if len(l3) == 0 { // No explicit layer 3: find the IP header by parsing the link layer. if ipOff, err = inet.FindIP(pkt, lt); err != nil { return nil, fmt.Errorf("fix lengths/checksums: %w (or uncheck the fix option)", err) } } if res.Fix, err = inet.Fix(pkt[ipOff:], hdrLen); err != nil { return nil, fmt.Errorf("fix lengths/checksums: %w (or uncheck the fix option)", err) } res.Fixed = true res.IPOffset = ipOff } res.Packet = pkt res.Dump = hexDump(pkt, orig, len(l2), len(l2)+len(l3)) return res, nil } func resolveLinkType(f hex2pcapForm, haveL2, haveL3 bool) (uint32, error) { switch f.LinkType { case "", "auto": if !haveL2 && haveL3 { return pcap.LinkTypeRaw, nil } return pcap.LinkTypeEthernet, nil case "custom": v, err := strconv.ParseUint(strings.TrimSpace(f.CustomLinkType), 0, 32) if err != nil { return 0, fmt.Errorf("invalid custom link type %q", f.CustomLinkType) } return uint32(v), nil } v, err := strconv.ParseUint(f.LinkType, 10, 32) if err != nil { return 0, fmt.Errorf("invalid link type %q", f.LinkType) } return uint32(v), nil } type dumpByte struct { Hex string Class string // l2, l3, l7 or empty for padding Changed bool Sep string // whitespace following the byte } type dumpRow struct { Offset string Bytes []dumpByte ASCII string } // hexDump renders pkt as hexdump rows, tagging each byte with the layer it // came from and whether the fixup changed it relative to orig. func hexDump(pkt, orig []byte, l3Start, l7Start int) []dumpRow { var rows []dumpRow for off := 0; off < len(pkt); off += 16 { row := dumpRow{Offset: fmt.Sprintf("%04x", off)} var ascii strings.Builder for i := off; i < off+16; i++ { sep := " " if i-off == 7 { sep = " " } if i >= len(pkt) { row.Bytes = append(row.Bytes, dumpByte{Hex: " ", Sep: sep}) continue } class := "l7" switch { case i < l3Start: class = "l2" case i < l7Start: class = "l3" } b := pkt[i] row.Bytes = append(row.Bytes, dumpByte{Hex: fmt.Sprintf("%02x", b), Class: class, Changed: b != orig[i], Sep: sep}) if b >= 0x20 && b < 0x7f { ascii.WriteByte(b) } else { ascii.WriteByte('.') } } row.ASCII = ascii.String() rows = append(rows, row) } return rows }