From 3f8f28dd9906d8f254129a6de59d50c3721219de Mon Sep 17 00:00:00 2001 From: UnicodingUnicorn <7555ic@gmail.com> Date: Wed, 6 Feb 2019 13:26:35 +0800 Subject: [PATCH] Initial commit --- .gitignore | 12 ++++ README.md | 81 ++++++++++++++++++++++++++- bite.pb.go | 93 +++++++++++++++++++++++++++++++ main.go | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 bite.pb.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2dd955 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/README.md b/README.md index ab68de9..e894f9f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,82 @@ # backend-publish -Beep backend accepts PUT requests and publishes them to NAT queue. \ No newline at end of file +Beep backend accepts PUT requests and publishes a protobuf-ed version to a [NATS](htts://nats.io) queue, like some sort of weird HTTP/NATS converter. Needless to say, relies on a NATS instance being up. + +## Quickstart + +``` +go build && ./backend-publish +``` + +## Flags + +Flags are supplied to the compiled go program in the form ```-flag=stuff```. + +| Flag | Description | Default | +| ---- | ----------- | ------- | +| listen | Port number to listen on | 8080 | +| nats | URL of NATS | nats://localhost:4222 | + +## API + +### Put Bite + +``` +PUT /conversation/:key/start/:start +``` + +TODO: Description of what this does cos honestly I have no idea Ambrose doesn't write documentation + +#### URL Params + +| Name | Type | Description | +| ---- | ---- | ----------- | +| key | String | Audio bite's conversation's ID. | +| start | Epoch timestamp | Time the audio bite starts. | + +#### Body + +Raw body of audio data in bytes. + +#### Success (200 OK) + +Empty body. + +#### Errors + +| Code | Description | +| ---- | ----------- | +| 400 | start is not an uint/key is not an alphanumeric string/data could not be read from the body | +| 500 | Error serialising data into a protocol buffer. | + +--- + +### Put Bite User + +``` +PUT /conversation/:key/start/:start/user +``` + +TODO: Description of what this does cos honestly I have no idea Ambrose doesn't write documentation + +#### URL Params + +| Name | Type | Description | +| ---- | ---- | ----------- | +| key | String | Audio bite's conversation's ID. | +| start | Epoch timestamp | Time the audio bite starts. | + +#### Body + +Raw body of audio data in bytes. + +#### Success (200 OK) + +Empty body. + +#### Errors + +| Code | Description | +| ---- | ----------- | +| 400 | start is not an uint/key is not an alphanumeric string/data could not be read from the body | +| 500 | Error serialising data into a protocol buffer. | diff --git a/bite.pb.go b/bite.pb.go new file mode 100644 index 0000000..cdc67b5 --- /dev/null +++ b/bite.pb.go @@ -0,0 +1,93 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: bite.proto + +package main + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Bite struct { + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Start uint64 `protobuf:"varint,2,opt,name=start,proto3" json:"start,omitempty"` + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Bite) Reset() { *m = Bite{} } +func (m *Bite) String() string { return proto.CompactTextString(m) } +func (*Bite) ProtoMessage() {} +func (*Bite) Descriptor() ([]byte, []int) { + return fileDescriptor_e1ec993646b17549, []int{0} +} + +func (m *Bite) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Bite.Unmarshal(m, b) +} +func (m *Bite) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Bite.Marshal(b, m, deterministic) +} +func (m *Bite) XXX_Merge(src proto.Message) { + xxx_messageInfo_Bite.Merge(m, src) +} +func (m *Bite) XXX_Size() int { + return xxx_messageInfo_Bite.Size(m) +} +func (m *Bite) XXX_DiscardUnknown() { + xxx_messageInfo_Bite.DiscardUnknown(m) +} + +var xxx_messageInfo_Bite proto.InternalMessageInfo + +func (m *Bite) GetKey() []byte { + if m != nil { + return m.Key + } + return nil +} + +func (m *Bite) GetStart() uint64 { + if m != nil { + return m.Start + } + return 0 +} + +func (m *Bite) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +func init() { + proto.RegisterType((*Bite)(nil), "main.Bite") +} + +func init() { proto.RegisterFile("bite.proto", fileDescriptor_e1ec993646b17549) } + +var fileDescriptor_e1ec993646b17549 = []byte{ + // 102 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4a, 0xca, 0x2c, 0x49, + 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d, 0xcc, 0xcc, 0x53, 0x72, 0xe2, 0x62, + 0x71, 0xca, 0x2c, 0x49, 0x15, 0x12, 0xe0, 0x62, 0xce, 0x4e, 0xad, 0x94, 0x60, 0x54, 0x60, 0xd4, + 0xe0, 0x09, 0x02, 0x31, 0x85, 0x44, 0xb8, 0x58, 0x8b, 0x4b, 0x12, 0x8b, 0x4a, 0x24, 0x98, 0x14, + 0x18, 0x35, 0x58, 0x82, 0x20, 0x1c, 0x21, 0x21, 0x2e, 0x96, 0x94, 0xc4, 0x92, 0x44, 0x09, 0x66, + 0xb0, 0x42, 0x30, 0x3b, 0x89, 0x0d, 0x6c, 0xa0, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xa3, 0xe1, + 0xe5, 0xfe, 0x5e, 0x00, 0x00, 0x00, +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e60e8f0 --- /dev/null +++ b/main.go @@ -0,0 +1,158 @@ +package main; + +import ( + "encoding/binary" + "errors" + "flag" + "io/ioutil" + "net/http" + "log" + "regexp" + "strconv" + + "github.com/golang/protobuf/proto" + "github.com/julienschmidt/httprouter" + "github.com/nats-io/go-nats" +) + +const MaxBiteSize = 1024 * 1024 * 10 + +var listen string +var natsHost string + +var nats_conn *nats.Conn + +func main() { + // Parse flags + flag.StringVar(&listen, "listen", ":8080", "host and port to listen on") + flag.StringVar(&natsHost, "nats", "nats://localhost:4222", "host and port of NATS") + flag.Parse() + + //NATS + n, err := nats.Connect(natsHost) + if err != nil { + log.Fatal(err) + return + } + nats_conn = n + + // Routes + router := httprouter.New() + + router.PUT("/conversation/:key/start/:start", PutBite) // bites + router.PUT("/conversation/:key/start/:start/user", PutBiteUser) // bite_users + + // Start server + log.Printf("starting server on %s", listen) + log.Fatal(http.ListenAndServe(listen, router)) +} + +// Marshalling keys and assorted helper functions +func validObj(obj string) bool { + return obj == "bite" || obj == "user" +} + +// TODO: ensure security of regexp +var validConversationRegexp = regexp.MustCompile(`^[a-zA-Z0-9-]+$`) + +func validConversation(conversation string) bool { + return validConversationRegexp.MatchString(conversation) +} + +const conversationSeprator = '@' +const objSeprator = '+' + +func MarshalKey(obj, conversation string, start uint64) ([]byte, error) { + prefixBytes, err := MarshalKeyPrefix(obj, conversation) + if err != nil { + return nil, err + } + + startBytes := make([]byte, 8) + binary.BigEndian.PutUint64(startBytes, start) + + return append(prefixBytes, startBytes...), nil +} + +func MarshalKeyPrefix(obj, conversation string) ([]byte, error) { + if !validObj(obj) || !validConversation(conversation) { + return nil, errors.New("main: FormatKey: bad obj or conversation") + } + return []byte(obj + string(objSeprator) + conversation + string(conversationSeprator)), nil +} + +func ParseStartString(start string) (uint64, error) { + return strconv.ParseUint(start, 10, 64) +} + +// Route handlers +func PutBite(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + start, err := ParseStartString(p.ByName("start")) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + key, err := MarshalKey("bite", p.ByName("key"), start) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + reader := http.MaxBytesReader(w, r.Body, MaxBiteSize) + body, err := ioutil.ReadAll(reader) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + b := Bite { + Start: start, + Key: key, + Data: body, + } + out, err := proto.Marshal(&b) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Print(err) + return + } + nats_conn.Publish("new_bite", out) + + w.WriteHeader(200) +} + +func PutBiteUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + start, err := ParseStartString(p.ByName("start")) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + key, err := MarshalKey("user", p.ByName("key"), start) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + reader := http.MaxBytesReader(w, r.Body, MaxBiteSize) + body, err := ioutil.ReadAll(reader) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + b := Bite { + Start: start, + Key: key, + Data: body, + } + out, err := proto.Marshal(&b) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Print(err) + return + } + nats_conn.Publish("new_bite_user", out) + + w.WriteHeader(200) +}