diff --git a/README.md b/README.md index 88d99be..b54c32d 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,87 @@ Example input JSON format: -d '{"Name":"Alice","Password":"Lewis"}' {"Status":"ok"} + +### Order + +Order creates a new order and lets other users know delegations are needed. + +Example input JSON format: + + $ curl --cacert server/server.crt https://localhost:8080/order \ + -d '{"Name":"Alice","Password":"Lewis","Labels": ["Blue","Red"],\ + "Duration":"1h","Uses":5,"EncryptedData":"ABCDE=="}' + { + "Admins": [ + "Bob", + "Eve" + ], + "AdminsDelegated": null, + "Delegated": 0, + "DurationRequested": 3.6e+12, + "Labels": [ + "blue", + "red" + ], + "Name": "Alice", + "Num": "77da1cfd8962fb9685c15c84", + "TimeRequested": "2016-01-25T15:58:41.961906679-08:00", + } + +### Orders Outstanding + +Orders Outstanding will return a list of current order numbers + +Example input JSON format: + + $ curl --cacert server/server.crt https://localhost:8080/orderout + -d '{"Name":"Alice","Password":"Lewis"}' + { + "77da1cfd8962fb9685c15c84":{ + "Name":"Alice", + "Num":"77da1cfd8962fb9685c15c84", + "TimeRequested":"2016-01-25T15:58:41.961906679-08:00", + "DurationRequested":3600000000000, + "Delegated":0," + AdminsDelegated":null, + "Admins":["Bob, Eve"], + "Labels":["Blue","Red"] + } + } + +### Order Information + +Example input JSON format: + + $ curl --cacert server/server.crt https://localhost:8080/orderinfo + -d '{"Name":"Alice","Password":"Lewis", \ + "OrderNum":"77da1cfd8962fb9685c15c84"}' + { + "Admins": [ + "Bob", + "Eve" + ], + "AdminsDelegated": null, + "Delegated": 0, + "DurationRequested": 3.6e+12, + "Labels": [ + "blue", + "red" + ], + "Name": "Alice", + "Num": "77da1cfd8962fb9685c15c84", + "TimeRequested": "2016-01-25T15:58:41.961906679-08:00" + } + +### Order Cancel + +Example input JSON format: + + $ curl --cacert server/server.crt https://localhost:8080/orderinfo + -d '{"Name":"Alice","Password":"Lewis", \ + "OrderNum":"77da1cfd8962fb9685c15c84"}' + {"Status":"ok"} + ### Web interface You can build a web interface to manage the Red October service using diff --git a/client/client.go b/client/client.go index c12566f..4255dd0 100644 --- a/client/client.go +++ b/client/client.go @@ -283,3 +283,63 @@ func (c *RemoteServer) Password(req []byte) (*core.ResponseData, error) { return unmarshalResponseData(respBytes) } + +// Order issues an order request to the remote server +func (c *RemoteServer) Order(req core.OrderRequest) (*core.ResponseData, error) { + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.doAction("order", reqBytes) + if err != nil { + return nil, err + } + + return unmarshalResponseData(respBytes) +} + +// OrderOutstanding issues an order outstanding request to the remote server +func (c *RemoteServer) OrderOutstanding(req core.OrderOutstandingRequest) (*core.ResponseData, error) { + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.doAction("orderout", reqBytes) + if err != nil { + return nil, err + } + + return unmarshalResponseData(respBytes) +} + +// OrderInfo issues an order info request to the remote server +func (c *RemoteServer) OrderInfo(req core.OrderInfoRequest) (*core.ResponseData, error) { + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.doAction("orderinfo", reqBytes) + if err != nil { + return nil, err + } + + return unmarshalResponseData(respBytes) +} + +// OrderCancel issues an order cancel request to the remote server +func (c *RemoteServer) OrderCancel(req core.OrderInfoRequest) (*core.ResponseData, error) { + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.doAction("ordercancel", reqBytes) + if err != nil { + return nil, err + } + + return unmarshalResponseData(respBytes) +} diff --git a/cmd/ro/main.go b/cmd/ro/main.go index 7aeaaba..1e6fa96 100644 --- a/cmd/ro/main.go +++ b/cmd/ro/main.go @@ -9,10 +9,12 @@ import ( "log" "os" "strings" + "time" "github.com/cloudflare/redoctober/client" "github.com/cloudflare/redoctober/cmd/ro/gopass" "github.com/cloudflare/redoctober/core" + "github.com/cloudflare/redoctober/order" ) var action, user, pswd, userEnv, pswdEnv, server, caPath string @@ -21,7 +23,9 @@ var owners, lefters, righters, inPath, labels, outPath, outEnv string var uses int -var time, users string +var duration, users string + +var pollInterval time.Duration type command struct { Run func() @@ -37,6 +41,7 @@ var commandSet = map[string]command{ "encrypt": command{Run: runEncrypt, Desc: "encrypt a file"}, "decrypt": command{Run: runDecrypt, Desc: "decrypt a file"}, "re-encrypt": command{Run: runReEncrypt, Desc: "re-encrypt a file"}, + "order": command{Run: runOrder, Desc: "place an order for delegations"}, } func registerFlags() { @@ -45,7 +50,7 @@ func registerFlags() { flag.StringVar(&owners, "owners", "", "comma separated owner list") flag.StringVar(&users, "users", "", "comma separated user list") flag.IntVar(&uses, "uses", 0, "number of delegated key uses") - flag.StringVar(&time, "time", "0h", "duration of delegated key uses") + flag.StringVar(&duration, "time", "0h", "duration of delegated key uses") flag.StringVar(&lefters, "left", "", "comma separated left owners") flag.StringVar(&righters, "right", "", "comma separated right owners") flag.StringVar(&labels, "labels", "", "comma separated labels") @@ -56,6 +61,7 @@ func registerFlags() { flag.StringVar(&pswd, "password", "", "password") flag.StringVar(&userEnv, "userenv", "RO_USER", "env variable for user name") flag.StringVar(&pswdEnv, "pswdenv", "RO_PASS", "env variable for user password") + flag.DurationVar(&pollInterval, "poll-interval", time.Second, "interval for polling an outstanding order (set 0 to disable polling)") } func getUserCredentials() { @@ -99,7 +105,7 @@ func runDelegate() { Name: user, Password: pswd, Uses: uses, - Time: time, + Time: duration, Users: processCSL(users), Labels: processCSL(labels), } @@ -207,6 +213,34 @@ func runDecrypt() { ioutil.WriteFile(outPath, msg.Data, 0644) } +func runOrder() { + req := core.OrderRequest{ + Name: user, + Password: pswd, + Uses: uses, + Duration: duration, + Labels: processCSL(labels), + Users: processCSL(users), + } + resp, err := roServer.Order(req) + processError(err) + + var o order.Order + err = json.Unmarshal(resp.Response, &o) + processError(err) + + if pollInterval > 0 { + for o.Delegated < 2 { + time.Sleep(pollInterval) + resp, err = roServer.OrderInfo(core.OrderInfoRequest{Name: user, Password: pswd, OrderNum: o.Num}) + processError(err) + err = json.Unmarshal(resp.Response, &o) + processError(err) + } + } + fmt.Println(resp.Status) +} + func main() { flag.Usage = func() { fmt.Println("Usage: ro [options] subcommand") diff --git a/core/core.go b/core/core.go index 51608ac..9f2d9ef 100644 --- a/core/core.go +++ b/core/core.go @@ -9,9 +9,13 @@ import ( "errors" "fmt" "log" + "strconv" + "time" "github.com/cloudflare/redoctober/cryptor" + "github.com/cloudflare/redoctober/hipchat" "github.com/cloudflare/redoctober/keycache" + "github.com/cloudflare/redoctober/order" "github.com/cloudflare/redoctober/passvault" ) @@ -19,6 +23,7 @@ var ( crypt cryptor.Cryptor records passvault.Records cache keycache.Cache + orders order.Orderer ) // Each of these structures corresponds to the JSON expected on the @@ -53,9 +58,10 @@ type DelegateRequest struct { } type CreateUserRequest struct { - Name string - Password string - UserType string + Name string + Password string + UserType string + HipchatName string } type PasswordRequest struct { @@ -63,6 +69,7 @@ type PasswordRequest struct { Password string NewPassword string + HipchatName string } type EncryptRequest struct { @@ -106,6 +113,34 @@ type ExportRequest struct { Password string } +type OrderRequest struct { + Name string + Password string + Duration string + Uses int + Users []string + EncryptedData []byte + Labels []string +} + +type OrderInfoRequest struct { + Name string + Password string + + OrderNum string +} +type OrderOutstandingRequest struct { + Name string + Password string +} + +type OrderCancelRequest struct { + Name string + Password string + + OrderNum string +} + // These structures map the JSON responses that will be sent from the API type ResponseData struct { @@ -169,8 +204,7 @@ func validateUser(name, password string, admin bool) error { return nil } -// validateName checks that the username and password pass the minimal -// validation check +// validateName checks that the username and password pass a validation test. func validateName(name, password string) error { if name == "" { return errors.New("User name must not be blank") @@ -183,7 +217,7 @@ func validateName(name, password string) error { } // Init reads the records from disk from a given path -func Init(path string) error { +func Init(path, hcKey, hcRoom, hcHost, roHost string) error { var err error defer func() { @@ -198,6 +232,20 @@ func Init(path string) error { err = fmt.Errorf("failed to load password vault %s: %s", path, err) } + var hipchatClient hipchat.HipchatClient + if hcKey != "" && hcRoom != "" && hcHost != "" { + roomId, err := strconv.Atoi(hcRoom) + if err != nil { + return errors.New("core.init unable to use hipchat roomId provided") + } + hipchatClient = hipchat.HipchatClient{ + ApiKey: hcKey, + RoomId: roomId, + HcHost: hcHost, + RoHost: roHost, + } + } + orders = order.NewOrderer(hipchatClient) cache = keycache.Cache{UserKeys: make(map[keycache.DelegateIndex]keycache.ActiveUser)} crypt = cryptor.New(&records, &cache) @@ -352,6 +400,32 @@ func Delegate(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } + // Make sure we capture the number who have already delegated. + for _, delegatedUser := range s.Users { + if orderKey, found := orders.FindOrder(delegatedUser, s.Labels); found { + order := orders.Orders[orderKey] + + // Don't re-add names to the list of people who have delegated. Instead + // just skip them but make sure we count their delegation + if len(order.OwnersDelegated) == 0 { + order.OwnersDelegated = append(order.OwnersDelegated, s.Name) + } else { + for _, ownerName := range order.OwnersDelegated { + if ownerName == s.Name { + continue + } + order.OwnersDelegated = append(order.OwnersDelegated, s.Name) + order.Delegated++ + } + } + orders.Orders[orderKey] = order + + // Notify the hipchat room that there was a new delegator + orders.NotifyDelegation(s.Name, delegatedUser, orderKey, s.Time, s.Labels) + + } + } + return jsonStatusOk() } @@ -393,10 +467,13 @@ func CreateUser(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - if _, err = records.AddNewRecord(s.Name, s.Password, false, s.UserType); err != nil { + if _, err := records.AddNewRecord(s.Name, s.Password, false, s.UserType); err != nil { return jsonStatusError(err) } + if err = records.ChangePassword(s.Name, s.Password, "", s.HipchatName); err != nil { + return jsonStatusError(err) + } return jsonStatusOk() } @@ -423,7 +500,7 @@ func Password(jsonIn []byte) ([]byte, error) { } // add signed-in record to active set - err = records.ChangePassword(s.Name, s.Password, s.NewPassword) + err = records.ChangePassword(s.Name, s.Password, s.NewPassword, s.HipchatName) if err != nil { return jsonStatusError(err) } @@ -490,7 +567,7 @@ func ReEncrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - data, _, secure, err := crypt.Decrypt(s.Data, s.Name) + data, _, _, secure, err := crypt.Decrypt(s.Data, s.Name) if err != nil { return jsonStatusError(err) } @@ -535,7 +612,7 @@ func Decrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } - data, names, secure, err := crypt.Decrypt(s.Data, s.Name) + data, allLabels, names, secure, err := crypt.Decrypt(s.Data, s.Name) if err != nil { return jsonStatusError(err) } @@ -551,6 +628,11 @@ func Decrypt(jsonIn []byte) ([]byte, error) { return jsonStatusError(err) } + // Cleanup any orders that have been fulfilled and notify the room. + if orderKey, found := orders.FindOrder(s.Name, allLabels); found { + delete(orders.Orders, orderKey) + orders.NotifyOrderFulfilled(s.Name, orderKey) + } return jsonResponse(out) } @@ -661,3 +743,162 @@ func Export(jsonIn []byte) ([]byte, error) { return jsonResponse(out) } + +// Order will request delegations from other users. +func Order(jsonIn []byte) (out []byte, err error) { + var o OrderRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + // Get the owners of the ciphertext. + owners, _, err := crypt.GetOwners(o.EncryptedData) + if err != nil { + jsonStatusError(err) + } + if o.Duration == "" { + err = errors.New("Duration required when placing an order.") + jsonStatusError(err) + } + if o.Uses == 0 { + err = errors.New("Number of required uses necessary when placing an order.") + jsonStatusError(err) + } + cache.Refresh() + orderNum := order.GenerateNum() + + if len(o.Users) == 0 { + err = errors.New("Must specify at least one user per order.") + jsonStatusError(err) + } + adminsDelegated, numDelegated := cache.DelegateStatus(o.Users[0], o.Labels, owners) + duration, err := time.ParseDuration(o.Duration) + if err != nil { + jsonStatusError(err) + } + currentTime := time.Now() + ord := order.CreateOrder(o.Name, + orderNum, + currentTime, + duration, + adminsDelegated, + owners, + o.Users, + o.Labels, + numDelegated) + orders.Orders[orderNum] = ord + out, err = json.Marshal(ord) + + // Get a map to any alternative name we want to notify + altOwners := records.GetAltNamesFromName(orders.AlternateName, owners) + + // Let everyone on hipchat know there is a new order. + orders.NotifyNewOrder(o.Duration, orderNum, o.Users, o.Labels, o.Uses, altOwners) + if err != nil { + return jsonStatusError(err) + } + return jsonResponse(out) +} + +// OrdersOutstanding will return a list of currently outstanding orders. +func OrdersOutstanding(jsonIn []byte) (out []byte, err error) { + var o OrderOutstandingRequest + + defer func() { + if err != nil { + log.Printf("core.ordersout failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.ordersout success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + out, err = json.Marshal(orders.Orders) + if err != nil { + return jsonStatusError(err) + } + return jsonResponse(out) +} + +// OrderInfo will return a list of currently outstanding order numbers. +func OrderInfo(jsonIn []byte) (out []byte, err error) { + var o OrderInfoRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + if ord, ok := orders.Orders[o.OrderNum]; ok { + if out, err = json.Marshal(ord); err != nil { + return jsonStatusError(err) + } else if len(out) == 0 { + return jsonStatusError(errors.New("No order with that number")) + } + + return jsonResponse(out) + } + + return jsonStatusError(errors.New("No order with that number")) +} + +// OrderCancel will cancel an order given an order num +func OrderCancel(jsonIn []byte) (out []byte, err error) { + var o OrderCancelRequest + + defer func() { + if err != nil { + log.Printf("core.order failed: user=%s %v", o.Name, err) + } else { + log.Printf("core.order success: user=%s", o.Name) + } + }() + + if err = json.Unmarshal(jsonIn, &o); err != nil { + return jsonStatusError(err) + } + + if err := validateUser(o.Name, o.Password, false); err != nil { + return jsonStatusError(err) + } + + if ord, ok := orders.Orders[o.OrderNum]; ok { + if o.Name == ord.Creator { + delete(orders.Orders, o.OrderNum) + out = []byte("Successfully removed order") + return jsonResponse(out) + } + } + err = errors.New("Invalid Order Number") + return jsonStatusError(err) +} diff --git a/core/core_test.go b/core/core_test.go index 9951f74..6b00f05 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -18,7 +18,7 @@ import ( func TestCreate(t *testing.T) { createJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") - Init("memory") + Init("memory", "", "", "", "") respJson, err := Create(createJson) if err != nil { @@ -66,7 +66,7 @@ func TestSummary(t *testing.T) { t.Fatalf("Error in summary of account with no vault, %v", s.Status) } - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault respJson, err = Create(createJson) @@ -220,7 +220,7 @@ func TestCreateUser(t *testing.T) { createUserECCJson := []byte("{\"Name\":\"Cat\",\"Password\":\"Cheshire\",\"UserType\":\"ECC\"}") createVaultJson := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\"}") - Init("memory") + Init("memory", "", "", "", "") // Check that users cannot be created before a vault is respJson, err := CreateUser(createUserJson) @@ -292,7 +292,7 @@ func TestPassword(t *testing.T) { delegateJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Olleh\",\"Time\":\"2h\",\"Uses\":1}") passwordJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Olleh\",\"NewPassword\":\"Hello\"}") - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -405,7 +405,7 @@ func TestEncryptDecrypt(t *testing.T) { encryptJson2 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Minimum\":2,\"Owners\":[\"Alice\",\"Bob\",\"Carol\"],\"Data\":\"SGVsbG8gSmVsbG8=\",\"Labels\":[\"blue\",\"red\"]}") encryptJson3 := []byte("{\"Name\":\"Alice\",\"Password\":\"Hello\",\"Minimum\":1,\"Owners\":[\"Alice\"],\"Data\":\"SGVsbG8gSmVsbG8=\"}") - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -632,7 +632,7 @@ func TestReEncrypt(t *testing.T) { delegateJson7 := []byte(`{"Name":"Carol","Password":"Hello","Time":"10s","Uses":2,"Users":["Alice"],"Labels":["red"]}`) encryptJson := []byte(`{"Name":"Carol","Password":"Hello","Minimum":2,"Owners":["Alice","Bob","Carol"],"Data":"SGVsbG8gSmVsbG8=","Labels":["blue"]}`) - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -812,7 +812,7 @@ func TestOwners(t *testing.T) { var s ResponseData var l OwnersData - Init("memory") + Init("memory", "", "", "", "") Create(delegateJson) Delegate(delegateJson2) @@ -868,7 +868,7 @@ func TestModify(t *testing.T) { modifyJson4 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"ToModify\":\"Alice\",\"Command\":\"revoke\"}") modifyJson5 := []byte("{\"Name\":\"Carol\",\"Password\":\"Hello\",\"ToModify\":\"Alice\",\"Command\":\"delete\"}") - Init("memory") + Init("memory", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData @@ -1059,7 +1059,7 @@ func TestStatic(t *testing.T) { t.Fatalf("Error closing file, %v", err) } - Init("/tmp/db1.json") + Init("/tmp/db1.json", "", "", "", "") // check for summary of initialized vault with new member var s ResponseData diff --git a/cryptor/cryptor.go b/cryptor/cryptor.go index 47b489c..86db588 100644 --- a/cryptor/cryptor.go +++ b/cryptor/cryptor.go @@ -524,14 +524,14 @@ func (c *Cryptor) Encrypt(in []byte, labels []string, access AccessStructure) (r } // Decrypt decrypts a file using the keys in the key cache. -func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, secure bool, err error) { +func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, labels, names []string, secure bool, err error) { // unwrap encrypted file var encrypted EncryptedData if err = json.Unmarshal(in, &encrypted); err != nil { return } if encrypted.Version != DEFAULT_VERSION && encrypted.Version != -1 { - return nil, nil, secure, errors.New("Unknown version") + return nil, nil, nil, secure, errors.New("Unknown version") } secure = encrypted.Version == -1 @@ -551,7 +551,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, return } if encrypted.VaultId != vaultId { - return nil, nil, secure, errors.New("Wrong vault") + return nil, nil, nil, secure, errors.New("Wrong vault") } // compute HMAC @@ -579,6 +579,7 @@ func (c *Cryptor) Decrypt(in []byte, user string) (resp []byte, names []string, aesCBC.CryptBlocks(clearData, encrypted.Data) resp, err = padding.RemovePadding(clearData) + labels = encrypted.Labels return } @@ -637,7 +638,6 @@ func (c *Cryptor) GetOwners(in []byte) (names []string, predicate string, err er addedNames[name] = true } } - predicate = encrypted.Predicate return diff --git a/cryptor/cryptor_test.go b/cryptor/cryptor_test.go index 4b744ac..fa4727c 100644 --- a/cryptor/cryptor_test.go +++ b/cryptor/cryptor_test.go @@ -83,7 +83,6 @@ func TestDuplicates(t *testing.T) { if err != nil { t.Fatalf("%v", err) } - c := Cryptor{&records, &cache} for _, name := range names { @@ -113,7 +112,7 @@ func TestDuplicates(t *testing.T) { t.Fatalf("%v", err) } - _, _, _, err := c.Decrypt(resp, name) + _, _, _, _, err := c.Decrypt(resp, name) if err == nil { t.Fatalf("That shouldn't have worked!") } diff --git a/hipchat/hipchat.go b/hipchat/hipchat.go new file mode 100644 index 0000000..f6451f4 --- /dev/null +++ b/hipchat/hipchat.go @@ -0,0 +1,67 @@ +package hipchat + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strconv" +) + +const ( + RedBackground = "red" + YellowBackground = "yellow" + GreenBackground = "green" + GrayBackground = "gray" + RandomBackground = "random" + PurpleBackground = "purple" +) + +type HipchatClient struct { + RoomId int + ApiKey string + HcHost string + RoHost string +} + +func NewClient() *HipchatClient { + return &HipchatClient{} +} +func (h *HipchatClient) Notify(msg, color string) error { + if h.ApiKey == "" { + return errors.New("ApiKey unset") + } + msgBody := map[string]interface{}{ + "message": msg, + "notify": "true", + "color": color, + } + + body, err := json.Marshal(msgBody) + if err != nil { + return err + } + roomId := url.QueryEscape(strconv.Itoa(h.RoomId)) + hipchatUrl := fmt.Sprintf("https://%s/v2/room/%s/notification?auth_token=%s", h.HcHost, roomId, h.ApiKey) + req, err := http.NewRequest("POST", hipchatUrl, bytes.NewReader(body)) + + req.Header.Add("Content-Type", "application/json") + + Client := http.Client{} + resp, err := Client.Do(req) + if err != nil { + log.Printf("Could not post to hipchat for the reason %s", err.Error()) + return err + } + + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("Could not post to hipchat for the reason %s", err.Error()) + return err + } + return nil +} diff --git a/index.html b/index.html index ea9ef7e..5d9dd6a 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@
  • Encrypt
  • Decrypt
  • Owners
  • +
  • Order
  • @@ -70,6 +71,8 @@ + +
    @@ -146,6 +149,12 @@
    +
    +
    + + +
    +
    @@ -164,7 +173,7 @@
    -

    Change password

    +

    Change account

    @@ -172,21 +181,25 @@
    - +
    - +
    - - + + +
    +
    + +
    -

    Modify user

    +

    Admin Controls

    @@ -309,6 +322,174 @@
    +
    +
    +
    +

    Create Order

    + +
    + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +

    Order Info

    + +
    + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +

    Outstanding Orders

    + +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +

    Order Cancel

    + +
    + +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    +

    Create Delegation Link

    + +
    + +
    + +