From 72d025f4bd0fc45116248fa21417ace2fd1682db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20K=C3=B6ritz?= Date: Mon, 26 Aug 2019 17:57:24 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + camera.sdp | 4 + ipcamera/camera.go | 212 +++++++++++++++++++++++++++++++++++++++++++ ipcamera/listener.go | 100 ++++++++++++++++++++ ipcamera/protocol.go | 60 ++++++++++++ main.go | 169 ++++++++++++++++++++++++++++++++++ 6 files changed, 548 insertions(+) create mode 100644 .gitignore create mode 100644 camera.sdp create mode 100644 ipcamera/camera.go create mode 100644 ipcamera/listener.go create mode 100644 ipcamera/protocol.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8b7925 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.mp4 +*.jpg +*.jpeg diff --git a/camera.sdp b/camera.sdp new file mode 100644 index 0000000..8e0e6a7 --- /dev/null +++ b/camera.sdp @@ -0,0 +1,4 @@ +v=0 +s=ActionCamera +m=video 5220 RTP/AVP 99 +a=rtpmap:99 H264/90000 diff --git a/ipcamera/camera.go b/ipcamera/camera.go new file mode 100644 index 0000000..46d3f0c --- /dev/null +++ b/ipcamera/camera.go @@ -0,0 +1,212 @@ +package ipcamera + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "log" + "net" + "strconv" + "strings" +) + +// Camera contains all information and features on a single IP Camera +type Camera struct { + IPAddress string + Port int + connected bool + disconnect bool + Verbose bool + connection net.Conn + receivedMessages chan Header + StoredFiles []StoredFile + fileList string +} + +// StoredFile is a file stored on the cameras sd-card +type StoredFile struct { + Path string + Size uint64 +} + +// Connect to the camera and start responding to keepalive packets +func (c *Camera) Connect(username, password string) { + if c.Verbose { + log.Printf("Connecting to %s:%d using username=%s, password=%s\n", c.IPAddress, c.Port, username, password) + } + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.IPAddress, c.Port)) + c.StoredFiles = make([]StoredFile, 0) + + if err != nil { + log.Printf("ERROR: %s\n", err) + } + c.receivedMessages = make(chan Header, 0) + c.connection = conn + + go cameraMessageHandler(c, conn) + + login(c, conn, username, password) +} + +func cameraMessageHandler(c *Camera, conn net.Conn) { + + header := Header{} + var payload []byte + for { + err := binary.Read(conn, binary.BigEndian, &header) + if err != nil { + break + } + + if header.Magic != 0xABCD { + log.Printf("Received message with invalid magic (%x).", header.Magic) + break + } + + //log.Printf("Received Header: %+v\n", header) + + if header.Length > 0 { + payload = make([]byte, header.Length) + bytesRead, err := io.ReadFull(conn, payload) + if err != nil { + log.Printf("Read Error: %s, %d bytes\n", err, bytesRead) + break + } + } else { + payload = []byte{} + } + + switch header.MessageType { + case 0x0111: // Login Accept + if c.Verbose { + log.Printf("Login Accepted") + } + requestFirmwareInfo(conn) + c.connected = true + case 0x0112: // Alive Request + sendAliveResponse(header, conn) + case 0xA026: // List of files + numParts := binary.LittleEndian.Uint32(payload[:4]) + currentPart := binary.LittleEndian.Uint32(payload[4:8]) + + c.fileList += string(payload[8:]) + if currentPart+1 >= numParts { + c.StoredFiles = parseFileList(c.fileList) + for _, file := range c.StoredFiles { + fmt.Printf("%s\t%d\n", file.Path, file.Size) + } + c.receivedMessages <- CreateCommandHeader(0xA027) // Dummy header to represent end of list + } + + case 0xA035: + if c.Verbose { + log.Printf("Received Firmware Info: %s\n", string(payload)) + } + c.StartPreviewStream() + case 0xA039: + if c.Verbose { + log.Printf("Took a still image and saved to SD-Card") + } + default: + log.Printf("Received Unknown Message: %+v\n", header) + log.Printf("Payload:\n%s\n", hex.Dump(payload)) + } + + select { + case c.receivedMessages <- header: + default: + } + + if c.disconnect { + break + } + } + c.connected = false +} + +func sendAliveResponse(request Header, conn net.Conn) { + request.MessageType = 0x0113 // Alive Response + response := CreatePacket(request, []byte{}) + conn.Write(response) +} + +func login(c *Camera, conn net.Conn, username, password string) { + login := CreateLoginPacket(username, password) + conn.Write(login) +} + +// RequestFileList instructs the camera to send a list of files from the camera +func (c *Camera) RequestFileList() { + c.fileList = "" + header := CreateCommandHeader(0xA025) + request := CreatePacket(header, []byte{0x01, 0x00, 0x00, 0x00}) + c.connection.Write(request) +} + +func parseFileList(input string) []StoredFile { + files := strings.Split(input, ";") + stored := make([]StoredFile, len(files)-1) + for i, file := range files { + parts := strings.Split(file, ":") + if len(parts) == 2 { + size, err := strconv.ParseUint(parts[1], 10, 64) + + if err == nil && size > 0 && len(parts[0]) > 0 { + stored[i] = StoredFile{ + Path: parts[0], + Size: size, + } + } + } + } + return stored +} + +func requestFirmwareInfo(conn net.Conn) { + conn.Write(CreateCommandPacket(0x0000A034)) +} + +// SendPacket sends a raw packet to the camera +func (c *Camera) SendPacket(packet []byte) { + c.connection.Write(packet) +} + +// TakePicture instructs the camera to take a still image +func (c *Camera) TakePicture() { + c.connection.Write(CreateCommandPacket(0x0000A038)) +} + +// StartPreviewStream starts streaming video to this host +func (c *Camera) StartPreviewStream() { + if c.Verbose { + log.Printf("Starting Preview Stream\n") + } + c.connection.Write(CreateCommandPacket(0x000001FF)) +} + +// StartRecording starts recording video to SD-Card +func (c *Camera) StartRecording() { + c.connection.Write(CreatePacket(CreateCommandHeader(0xA03A), []byte{0x01, 0x00, 0x00, 0x00})) +} + +// StopRecording stops recording video to SD-Card +func (c *Camera) StopRecording() { + c.connection.Write(CreatePacket(CreateCommandHeader(0xA03A), []byte{0x00, 0x00, 0x00, 0x00})) +} + +// Disconnect from the camera +func (c *Camera) Disconnect() { + c.disconnect = true + c.connected = false +} + +// WaitForMessage waits for a message of a specific type to arrive +func (c *Camera) WaitForMessage(packetType uint32) { + for { + header := <-c.receivedMessages + if header.MessageType == packetType { + return + } + } +} diff --git a/ipcamera/listener.go b/ipcamera/listener.go new file mode 100644 index 0000000..9ebe82a --- /dev/null +++ b/ipcamera/listener.go @@ -0,0 +1,100 @@ +package ipcamera + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "io" + "log" + "net" +) + +// StreamListener holds information on the receiving stream listener +type StreamListener struct { + close bool +} + +// CreateStreamListener creates a UDP listener that handles live data from the camera +func CreateStreamListener() StreamListener { + conn, err := net.ListenPacket("udp", ":6669") + + if err != nil { + log.Printf("ERROR: %s\n", err) + } + + streamListener := StreamListener{} + if err != nil { + log.Printf("ERROR: %s\n", err) + } + + go handleCameraStream(streamListener, conn) + + return streamListener +} + +func handleCameraStream(listener StreamListener, conn net.PacketConn) { + buffer := make([]byte, 2048) + header := StreamHeader{} + var payload []byte + + rtpTarget := net.UDPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 5220, + } + rtpSource, _ := net.ResolveUDPAddr("udp", "127.0.0.1:5000") + rtpConn, _ := net.DialUDP("udp", rtpSource, &rtpTarget) + + var sequenceNumber uint16 + var elapsed uint32 + + frameBuffer := []byte{} + + for { + conn.ReadFrom(buffer) + packetReader := bytes.NewReader(buffer) + binary.Read(packetReader, binary.BigEndian, &header) + + if header.Magic != 0xBCDE { + log.Printf("Received message with invalid magic (%x).", header.Magic) + break + } + if header.Length > 0 { + payload = make([]byte, header.Length) + bytesRead, err := io.ReadFull(packetReader, payload) + if err != nil { + log.Printf("Read Error: %s, %d bytes\n", err, bytesRead) + break + } + } else { + payload = []byte{} + } + + switch header.MessageType { + case 0x0001: // H.264 Data + frameBuffer = append(frameBuffer, payload...) + case 0x0002: // Time + packet := bytes.Buffer{} + packet.Write([]byte{0x80, 0x63}) + binary.Write(&packet, binary.BigEndian, sequenceNumber) + binary.Write(&packet, binary.BigEndian, (uint32)(elapsed*90)) + binary.Write(&packet, binary.BigEndian, (uint64(0))) + packet.Write(frameBuffer) + rtpConn.Write(packet.Bytes()) + frameBuffer = []byte{} + sequenceNumber++ + elapsed = binary.LittleEndian.Uint32(payload[12:]) + //log.Printf("Elapsed: %d (%x)\n", elapsed, payload[12:]) + default: + log.Printf("Received Unknown Message: %+v\n", header) + log.Printf("Payload:\n%s\n", hex.Dump(payload)) + } + if listener.close { + break + } + } +} + +// Close stops listening for packets +func (l *StreamListener) Close() { + l.close = true +} diff --git a/ipcamera/protocol.go b/ipcamera/protocol.go new file mode 100644 index 0000000..972a646 --- /dev/null +++ b/ipcamera/protocol.go @@ -0,0 +1,60 @@ +package ipcamera + +import ( + "bytes" + + "github.com/icza/bitio" +) + +// Header is an ipcamera protocol message header +type Header struct { + Magic uint16 + Length uint16 + MessageType uint32 +} + +// StreamHeader is a live preview message header +type StreamHeader struct { + Magic uint16 + Length uint16 + SequenceNumber uint16 + MessageType uint16 +} + +// CreatePacket creates a packet ready to be sent to the camera +func CreatePacket(header Header, payload []byte) []byte { + header.Length = (uint16)(len(payload)) + + buf := &bytes.Buffer{} + w := bitio.NewWriter(buf) + w.WriteBits((uint64)(header.Magic), 16) + w.WriteBits((uint64)(header.Length), 16) + w.WriteBits((uint64)(header.MessageType), 32) + w.Write(payload) + return buf.Bytes() +} + +// CreateCommandHeader prepares a packet header for command packets +func CreateCommandHeader(command uint32) Header { + return Header{ + Magic: 0xABCD, + Length: 0, + MessageType: command, + } +} + +// CreateLoginPacket creates a Login packet to be sent to the camera +func CreateLoginPacket(username, password string) []byte { + header := CreateCommandHeader(0x00000110) // Login + payload := make([]byte, 128) + copy(payload, []byte(username)) + copy(payload[64:], []byte(password)) + + return CreatePacket(header, payload) +} + +// CreateCommandPacket prepares a command packet to be sent to the camera +func CreateCommandPacket(command uint32) []byte { + header := CreateCommandHeader(command) + return CreatePacket(header, []byte{}) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..dffcf45 --- /dev/null +++ b/main.go @@ -0,0 +1,169 @@ +package main + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "io" + "log" + "net/http" + "os" + "path/filepath" + + "github.com/jonas-koeritz/libipcamera/ipcamera" + "github.com/spf13/cobra" +) + +func main() { + var username string + var password string + var port int16 + + var rootCmd = &cobra.Command{ + Use: "ipcamera [Cameras IP Address]", + Short: "ipcamera is a tool to stream the video preview of cheap action cameras without the mobile application", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + listener := ipcamera.CreateStreamListener() + camera := ipcamera.Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} + + log.Printf("Using Camera: %+v\n", camera) + + camera.Connect(username, password) + bufio.NewReader(os.Stdin).ReadBytes('\n') + listener.Close() + }, + } + + rootCmd.PersistentFlags().Int16VarP(&port, "port", "P", 6666, "Specify an alternative camera port to connect to") + rootCmd.PersistentFlags().StringVarP(&username, "username", "u", "admin", "Specify the camera username") + rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "12345", "Specify the camera password") + + var ls = &cobra.Command{ + Use: "ls [Cameras IP Address]", + Short: "List files stored on the cameras SD-Card", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + camera := ipcamera.Camera{IPAddress: args[0], Port: (int)(port), Verbose: false} + + camera.Connect(username, password) + camera.RequestFileList() + camera.WaitForMessage(0xA027) + //camera.StoredFiles + }, + } + + var still = &cobra.Command{ + Use: "still [Cameras IP Address]", + Short: "Take a still image and save to SD-Card", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + camera := ipcamera.Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} + camera.Connect(username, password) + camera.TakePicture() + camera.WaitForMessage(0xA039) + }, + } + + var record = &cobra.Command{ + Use: "record [Cameras IP Address]", + Short: "Start recording video to SD-Card", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + camera := ipcamera.Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} + camera.Connect(username, password) + camera.StartRecording() + camera.WaitForMessage(0xA03B) + }, + } + + var stop = &cobra.Command{ + Use: "stop [Cameras IP Address]", + Short: "Stop recording video to SD-Card", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + camera := ipcamera.Camera{IPAddress: args[0], Port: (int)(port), Verbose: true} + camera.Connect(username, password) + camera.StopRecording() + camera.WaitForMessage(0xA03B) + }, + } + + var cmd = &cobra.Command{ + Use: "cmd [RAW Command] [Cameras IP Address]", + Short: "Send a raw command to the camera", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + camera := ipcamera.Camera{IPAddress: args[1], Port: (int)(port), Verbose: true} + + camera.Connect(username, password) + command, err := hex.DecodeString(args[0]) + if err != nil { + log.Printf("ERROR: %s\n", err) + return + } + + if len(command) >= 2 { + header := ipcamera.CreateCommandHeader(uint32(binary.BigEndian.Uint16(command[:2]))) + payload := command[2:] + log.Printf("Waiting for Login to finish") + camera.WaitForMessage(0x0111) + packet := ipcamera.CreatePacket(header, payload) + log.Printf("Sending Command: %X\n", packet) + camera.SendPacket(packet) + } + + bufio.NewReader(os.Stdin).ReadBytes('\n') + }, + } + + var fetch = &cobra.Command{ + Use: "fetch [Cameras IP Address]", + Short: "List files stored on the cameras SD-Card", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + camera := ipcamera.Camera{IPAddress: args[0], Port: (int)(port), Verbose: false} + + camera.Connect(username, password) + camera.RequestFileList() + camera.WaitForMessage(0xA027) + newestFile := camera.StoredFiles[len(camera.StoredFiles)-1].Path + url := "http://" + args[0] + newestFile + log.Printf("Downloading latest File: %s\n", url) + downloadFile(filepath.Base(newestFile), url) + }, + } + + rootCmd.AddCommand(ls) + rootCmd.AddCommand(cmd) + rootCmd.AddCommand(still) + rootCmd.AddCommand(stop) + rootCmd.AddCommand(fetch) + rootCmd.AddCommand(record) + + if err := rootCmd.Execute(); err != nil { + log.Println(err) + os.Exit(1) + } +} + +func downloadFile(filepath string, url string) error { + + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // Create the file + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + return err +}