Files
pcap-web-tools/internal/web/hex2pcap.go
T
2026-10-05 20:07:31 +02:00

269 lines
7.0 KiB
Go

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
}
// pcapFilename sanitizes a user supplied download name.
func pcapFilename(name string) string {
name = strings.TrimSuffix(strings.TrimSpace(name), ".pcap")
name = strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
return r
}
return '_'
}, name)
if strings.Trim(name, "._") == "" {
name = "packet"
}
return name + ".pcap"
}
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
}