Add hipchat and ordering support to redoctober. Also fix XSS in RO

Supports MSP and requires several arguments to add hipchat integration to
red october. RedOctober will then alert on creation of an order, any new
delegation, or several other states.
This commit is contained in:
e
2015-08-04 14:01:04 -07:00
committed by ejcx
parent 1bfa291c37
commit 95940ed3fa
11 changed files with 1042 additions and 57 deletions

View File

@@ -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
@@ -63,6 +68,7 @@ type PasswordRequest struct {
Password string
NewPassword string
HipchatName string
}
type EncryptRequest struct {
@@ -106,6 +112,33 @@ type ExportRequest struct {
Password string
}
type OrderRequest struct {
Name string
Password string
Duration string
Uses string
Data []byte
Label 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 +202,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,21 +215,35 @@ func validateName(name, password string) error {
}
// Init reads the records from disk from a given path
func Init(path string) error {
func Init(ca, hcKey, hcRoom, hcHost, roHost string) error {
var err error
defer func() {
if err != nil {
log.Printf("core.init failed: %v", err)
} else {
log.Printf("core.init success: path=%s", path)
log.Printf("core.init success: ca=%s", ca)
}
}()
if records, err = passvault.InitFrom(path); err != nil {
err = fmt.Errorf("failed to load password vault %s: %s", path, err)
if records, err = passvault.InitFrom(ca); err != nil {
err = fmt.Errorf("failed to load password vault %s: %s", ca, 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 +398,22 @@ func Delegate(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
// Make sure we capture the number who have already delegated.
for _, delegatedUser := range s.Users {
for _, delegatedLabel := range s.Labels {
if orderKey, found := orders.FindOrder(delegatedUser, delegatedLabel); found {
order := orders.Orders[orderKey]
order.AdminsDelegated = append(order.AdminsDelegated, s.Name)
order.Delegated++
orders.Orders[orderKey] = order
// Notify the hipchat room that there was a new delegator
orders.NotifyDelegation(s.Name, delegatedLabel, delegatedUser, orderKey, s.Time)
}
}
}
return jsonStatusOk()
}
@@ -423,7 +485,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 +552,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 +597,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 +613,13 @@ func Decrypt(jsonIn []byte) ([]byte, error) {
return jsonStatusError(err)
}
// Cleanup any orders that have been fulfilled and notify the room.
for _, label := range allLabels {
if orderKey, found := orders.FindOrder(s.Name, label); found {
delete(orders.Orders, orderKey)
orders.NotifyOrderFulfilled(s.Name, orderKey)
}
}
return jsonResponse(out)
}
@@ -661,3 +730,158 @@ 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.Data)
if err != nil {
jsonStatusError(err)
}
if o.Duration == "" {
err = errors.New("Duration required when placing an order.")
jsonStatusError(err)
}
if o.Uses == "" || o.Uses == "0" {
err = errors.New("Number of required uses necessary when placing an order.")
jsonStatusError(err)
}
cache.Refresh()
orderNum := order.GenerateNum()
adminsDelegated, numDelegated := cache.DelegateStatus(o.Name, o.Label, owners)
duration, err := time.ParseDuration(o.Duration)
if err != nil {
jsonStatusError(err)
}
currentTime := time.Now()
expiryTime := currentTime.Add(duration)
ord := order.CreateOrder(o.Name,
o.Label,
orderNum,
currentTime,
expiryTime,
duration,
adminsDelegated,
owners,
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.Name, o.Duration, o.Label, o.Uses, orderNum, 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
}
// 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.Name {
delete(orders.Orders, o.OrderNum)
out = []byte("Successfully removed order")
return jsonResponse(out)
}
}
err = errors.New("Invalid Order Number")
return jsonStatusError(err)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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!")
}

67
hipchat/hipchat.go Normal file
View File

@@ -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
}

View File

@@ -27,6 +27,7 @@
<li><a href="#encrypt">Encrypt</a></li>
<li><a href="#decrypt">Decrypt</a></li>
<li><a href="#owners">Owners</a></li>
<li><a href="#orders">Order</a></li>
</ul>
</div>
</div>
@@ -164,7 +165,7 @@
<section class="row">
<div id="change-password" class="col-md-6">
<h3>Change password</h3>
<h3>Change account</h3>
<form id="user-change-password" class="ro-user-change-password" role="form" action="/password" method="post">
<div class="feedback change-password-feedback"></div>
@@ -172,16 +173,18 @@
<div class="form-group row">
<div class="col-md-6">
<label for="user-name">User name</label>
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required />
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required/>
</div>
<div class="col-md-6">
<label for="user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password" required />
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password"/ required>
</div>
</div>
<div class="form-group">
<label for="user-pass">New password</label>
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New password" required />
<label for="user-pass">New password. Blank for no change.</label>
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New Password"/>
<label for="user-email">Hipchat Name. Blank for no change.</label>
<input type="text" name="HipchatName" class="form-control" id="user-hipchatname" placeholder="New Hipchat Name"/>
</div>
<button type="submit" class="btn btn-primary">Change password</button>
</form>
@@ -309,6 +312,120 @@
</form>
</div>
</section>
<hr />
<section class="row">
<div id="orders" class="col-md-6">
<h3>Create Order</h3>
<form id="order" class="ro-user-order" role="form" action="/order" method="post">
<div class="feedback order-feedback"></div>
<div class="form-group">
<div class="form-group row">
<div class="col-md-6">
<label for="order-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="order-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="order-user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="order-user-pass" placeholder="Password" required />
</div>
<div class="col-md-6">
<label for="order-label">Label</label>
<input type="text" name="Label" class="form-control" id="order-user-label" placeholder="Label" required />
</div>
<div class="col-md-6">
<label for="order-duration">Duration</label>
<input type="text" name="Duration" class="form-control" id="order-duration" placeholder="Duration (e.g., 2h34m)" required />
</div>
<div class="col-md-6">
<label for="order-uses">Uses</label>
<input type="text" name="Uses" class="form-control" id="order-uses" placeholder="Uses" required />
</div>
</div>
<label for="owners-data">Data</label>
<textarea name="Data" class="form-control" id="owners-data" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Create Order</button>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="ordersinfo" class="col-md-6">
<h3>Order Info</h3>
<form id="orderinfo" class="ro-user-order" role="form" action="/orderinfo" method="post">
<div style="overflow-wrap: break-word;" class="feedback orderinfo-feedback"></div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label for="orderinfo-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="orderinfo-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="orderinfo-user-admin">Password</label>
<input type="password" name="Password" class="form-control" id="orderinfo-user-pass" placeholder="Password" required />
</div>
<div class="col-md-6">
<label for="orderinfo-order-num">Order Num</label>
<input type="text" name="OrderNum" class="form-control" id="orderinfo-user-label" placeholder="Order Number" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Order Info</button>
</div>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="ordersout" class="col-md-6">
<h3>Outstanding Orders</h3>
<form id="orderout" class="ro-user-order" role="form" action="/orderout" method="post">
<div style="overflow-wrap: break-word;" class="feedback ordersout-feedback"></div>
<div class="form-group">
<div class="form-group row">
<div class="col-md-6">
<label for="ordersout-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="ordersout-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="ordersout-user-admin">Password</label>
<input type="password" name="Password" class="form-control" id="ordersout-user-pass" placeholder="Password" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Outstanding Orders</button>
</form>
</div>
</section>
<section class="row">
<div id="orderscancel" class="col-md-6">
<h3>Order Cancel</h3>
<form id="ordercancel" class="ro-user-order" role="form" action="/ordercancel" method="post">
<div style="overflow-wrap: break-word;" class="feedback ordercancel-feedback"></div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label for="ordercancel-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="ordercancel-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="ordercancel-user-admin">Password</label>
<input type="password" name="Password" class="form-control" id="ordercancel-user-pass" placeholder="Password" required />
</div>
<div class="col-md-6">
<label for="ordercancel-order-num">Order Num</label>
<input type="text" name="OrderNum" class="form-control" id="ordercancel-user-label" placeholder="Order Number" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Order Cancel</button>
</div>
</form>
</div>
</section>
<hr />
</div>
<footer id="footer" class="footer">
@@ -327,7 +444,6 @@
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
@@ -364,7 +480,7 @@
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+data.Name }) );
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -425,7 +541,7 @@
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+data.Name }) );
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -442,7 +558,7 @@
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+data.Name }) );
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -456,7 +572,7 @@
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+data.Name }) );
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -470,7 +586,7 @@
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+data.ToModify }) );
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+htmlspecialchars(data.ToModify) }) );
}
});
});
@@ -532,6 +648,140 @@
}
});
});
// Create an order
$('body').on('submit', 'form#order', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = JSON.parse(window.atob(d.Response));
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>Order Num: '+d.Num+'</p>' }) );
}
});
});
// Get order info
$('body').on('submit', 'form#orderinfo', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = window.atob(d.Response);
try {
var respData = JSON.parse(d);
var msgText = ""
alert(msgText);
for (var jj in respData) {
if (!jj || jj == "Admins")
continue;
if (!respData.hasOwnProperty(jj)) {
continue;
}
alert(msgText);
msgText += "<p>"+htmlspecialchars(jj)+": "+htmlspecialchars(respData[jj])+"</p>";
}
$form.find('.feedback').empty().append(makeAlert({ type: 'success', message: msgText }));
} catch (e) {
makeAlert({ type: 'failure', message: '<p>Invalid JSON returned</p>' });
}
}
});
});
// Get outstanding order info
$('body').on('submit', 'form#orderout', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = JSON.parse(window.atob(d.Response));
ordout = "";
for (var jj in d){
if (!d.hasOwnProperty(jj))
continue;
var o = d[jj];
ordout += o.Name + " requesting " + o.Label + " has " + o.Delegated + "\n";
}
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>'+ordout+'</p>' }) );
}
});
});
$('body').on('submit', 'form#ordercancel', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = window.atob(d.Response);
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>'+d+'</p>' }) );
}
});
});
// Init from query string if possible.
var queryParams = document.location.search;
var queryParts = queryParams.split('&');
for (var i=0; i<queryParts.length; i++) {
var part = queryParts[i];
part = part.replace("?", "");
var partPieces = part.split("=");
if (partPieces.length != 2) {
continue;
}
var setValue = null;
var key = partPieces[0];
var value = partPieces[1];
switch (key) {
case "delegator":
setValue = $("#delegate-user");
break;
case "delegatee":
setValue = $("#delegate-users");
break;
case "uses":
setValue = $("#delegate-uses");
break;
case "label":
setValue = $("#delegate-labels");
break;
case "duration":
setValue = $("#delegate-user-time");
break;
case "ordernum":
setValue = $("#delegate-slot");
break;
default:
break;
}
if (setValue) {
setValue.val(value);
}
}
function htmlspecialchars(s) {
if (!isNaN(s)) {
return s;
}
s = s.replace('&', '&amp;');
s = s.replace('<', '&lt;');
s = s.replace('>', '&gt;');
s = s.replace('"', '&quot;');
s = s.replace("'", '&#x27;');
s = s.replace('/', '&#x2F;');
return s
}
});
</script>
</body>

View File

@@ -40,8 +40,9 @@ type Usage struct {
// ActiveUser holds the information about an actively delegated key.
type ActiveUser struct {
Usage
Admin bool
Type string
AltNames map[string]string
Admin bool
Type string
rsaKey rsa.PrivateKey
eccKey *ecdsa.PrivateKey
@@ -305,3 +306,35 @@ func (cache *Cache) DecryptShares(in [][]byte, name, user string, labels []strin
return
}
// DelegateStatus will return a list of admins who have delegated to a particular user, for a particular label.
// This is useful information to have when determining the status of an order and conveying order progress.
func (cache *Cache) DelegateStatus(name string, label string, admins []string) (adminsDelegated []string, hasDelegated int) {
// Iterate over the admins of the ciphertext to look for users
// who have already delegated the label to the delegatee.
for _, admin := range admins {
for di, use := range cache.UserKeys {
if di.Name != admin {
continue
}
labelFound := false
nameFound := false
for _, user := range use.Users {
if user == name {
nameFound = true
}
}
for _, l := range use.Labels {
if l == label {
labelFound = true
}
}
if labelFound && nameFound {
adminsDelegated = append(adminsDelegated, admin)
hasDelegated++
}
}
}
return
}

119
order/order.go Normal file
View File

@@ -0,0 +1,119 @@
package order
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/url"
"time"
"github.com/cloudflare/redoctober/hipchat"
)
const (
NewOrder = "%s has created an order for the label %s. requesting %s delegations for %s"
NewOrderLink = "@%s - https://%s?%s"
OrderFulfilled = "%s has had order %s fulfilled."
NewDelegation = "%s has delegated the label %s to %s (per order %s) for %s"
)
type Order struct {
Name string
Num string
TimeRequested time.Time
ExpiryTime time.Time
DurationRequested time.Duration
Delegated int
AdminsDelegated []string
Admins []string
Label string
}
type OrderIndex struct {
OrderFor string
OrderId string
OrderOwners []string
}
// Orders represents a mapping of Order IDs to Orders. This structure
// is useful for looking up information about individual Orders and
// whether or not an order has been fulfilled. Orders that have been
// fulfilled will be removed from the structure.
type Orderer struct {
Orders map[string]Order
Hipchat hipchat.HipchatClient
AlternateName string
}
func CreateOrder(name string, labels string, orderNum string, time time.Time, expiryTime time.Time, duration time.Duration, adminsDelegated, contacts []string, numDelegated int) (ord Order) {
ord.Name = name
ord.Num = orderNum
ord.Label = labels
ord.TimeRequested = time
ord.ExpiryTime = expiryTime
ord.DurationRequested = duration
ord.AdminsDelegated = adminsDelegated
ord.Admins = contacts
ord.Delegated = numDelegated
return
}
func GenerateNum() (num string) {
b := make([]byte, 12)
rand.Read(b)
return hex.EncodeToString(b)
}
// NewOrder will create a new map of Orders
func NewOrderer(hipchatClient hipchat.HipchatClient) (o Orderer) {
o.Orders = make(map[string]Order)
o.Hipchat = hipchatClient
o.AlternateName = "HipchatName"
return
}
// notify is a generic function for using a notifier, but it checks to make
// sure that there is a notifier available, since there won't always be.
func notify(o *Orderer, msg, color string) {
o.Hipchat.Notify(msg, color)
}
func (o *Orderer) NotifyNewOrder(name, duration, label, uses, orderNum string, owners map[string]string) {
n := fmt.Sprintf(NewOrder, name, label, uses, duration)
notify(o, n, hipchat.RedBackground)
for owner, hipchatName := range owners {
queryParams := url.Values{
"delegator": {owner},
"label": {label},
"duration": {duration},
"uses": {uses},
"ordernum": {orderNum},
"delegatee": {name},
}.Encode()
notify(o, fmt.Sprintf(NewOrderLink, hipchatName, o.Hipchat.RoHost, queryParams), hipchat.GreenBackground)
}
}
func (o *Orderer) NotifyDelegation(delegator, label, delegatee, orderNum, duration string) {
n := fmt.Sprintf(NewDelegation, delegator, label, delegatee, orderNum, duration)
notify(o, n, hipchat.YellowBackground)
}
func (o *Orderer) NotifyOrderFulfilled(name, orderNum string) {
n := fmt.Sprintf(OrderFulfilled, name, orderNum)
notify(o, n, hipchat.PurpleBackground)
}
func (o *Orderer) FindOrder(name, label string) (string, bool) {
for key, order := range o.Orders {
if name != order.Name {
continue
}
if label != order.Label {
continue
}
return key, true
}
return "", false
}

View File

@@ -85,7 +85,8 @@ type PasswordRecord struct {
ECPrivIV []byte
ECPublic ECPublicKey
}
Admin bool
AltNames map[string]string
Admin bool
}
// diskRecords is the structure used to read and write a JSON file
@@ -187,6 +188,8 @@ func createPasswordRec(password string, admin bool, userType string) (newRec Pas
return
}
newRec.AltNames = make(map[string]string)
// generate a key pair
switch userType {
case RSARecord:
@@ -273,7 +276,6 @@ func InitFrom(path string) (records Records, err error) {
// from the file.
records.Version = 0
if len(jsonDiskRecord) != 0 {
if err = json.Unmarshal(jsonDiskRecord, &records); err != nil {
return
@@ -281,7 +283,12 @@ func InitFrom(path string) (records Records, err error) {
}
err = errors.New("Format error")
for _, rec := range records.Passwords {
for k, rec := range records.Passwords {
if rec.AltNames == nil {
rec.AltNames = make(map[string]string)
records.Passwords[k] = rec
}
if len(rec.PasswordSalt) != 16 {
return
}
@@ -364,8 +371,16 @@ func (records *Records) AddNewRecord(name, password string, admin bool, userType
}
// ChangePassword changes the password for a given user.
func (records *Records) ChangePassword(name, password, newPassword string) (err error) {
func (records *Records) ChangePassword(name, password, newPassword, hipchatName string) (err error) {
pr, ok := records.GetRecord(name)
if len(newPassword) == 0 {
if len(hipchatName) != 0 {
pr.AltNames["HipchatName"] = hipchatName
}
records.SetRecord(pr, name)
return records.WriteRecordsToDisk()
}
if !ok {
err = errors.New("Record not present")
return
@@ -609,6 +624,26 @@ func (pr *PasswordRecord) GetKeyRSA(password string) (key rsa.PrivateKey, err er
return
}
func (r *Records) GetAltNameFromName(alt, name string) (altName string, found bool) {
if passwordRecord, ok := r.Passwords[name]; ok {
if altName, ok := passwordRecord.AltNames[alt]; ok {
return altName, true
}
}
return "", false
}
func (r *Records) GetAltNamesFromName(alt string, names []string) map[string]string {
altNames := make(map[string]string)
for _, name := range names {
altName, found := r.GetAltNameFromName(alt, name)
if !found {
altName = name
}
altNames[name] = altName
}
return altNames
}
// ValidatePassword returns an error if the password is incorrect.
func (pr *PasswordRecord) ValidatePassword(password string) error {

View File

@@ -98,7 +98,7 @@ func TestChangePassword(t *testing.T) {
}
// Check changing the password for a non-existent user
err = records.ChangePassword("user", "weakpassword", "newpassword")
err = records.ChangePassword("user", "weakpassword", "newpassword", "")
if err == nil {
t.Fatalf("%v", err)
}
@@ -108,7 +108,7 @@ func TestChangePassword(t *testing.T) {
t.Fatalf("%v", err)
}
err = records.ChangePassword("user", "weakpassword", "newpassword")
err = records.ChangePassword("user", "weakpassword", "newpassword", "")
if err != nil {
t.Fatalf("%v", err)
}
@@ -118,7 +118,7 @@ func TestChangePassword(t *testing.T) {
t.Fatalf("%v", err)
}
err = records.ChangePassword("user2", "weakpassword", "newpassword")
err = records.ChangePassword("user2", "weakpassword", "newpassword", "")
if err != nil {
t.Fatalf("%v", err)
}

View File

@@ -29,10 +29,10 @@ import (
var functions = map[string]func([]byte) ([]byte, error){
"/create": core.Create,
"/create-user": core.CreateUser,
"/summary": core.Summary,
"/purge": core.Purge,
"/delegate": core.Delegate,
"/create-user": core.CreateUser,
"/password": core.Password,
"/encrypt": core.Encrypt,
"/re-encrypt": core.ReEncrypt,
@@ -40,6 +40,10 @@ var functions = map[string]func([]byte) ([]byte, error){
"/owners": core.Owners,
"/modify": core.Modify,
"/export": core.Export,
"/order": core.Order,
"/orderout": core.OrdersOutstanding,
"/orderinfo": core.OrderInfo,
"/ordercancel": core.OrderCancel,
}
type userRequest struct {
@@ -220,6 +224,10 @@ func main() {
var certsPathString = flag.String("certs", "", "Path(s) of TLS certificate in PEM format, comma-separated")
var keysPathString = flag.String("keys", "", "Path(s) of TLS private key in PEM format, comma-separated, must me in the same order as the certs")
var caPath = flag.String("ca", "", "Path of TLS CA for client authentication (optional)")
var hcKey = flag.String("hckey", "", "Hipchat API Key")
var hcRoom = flag.String("hcroom", "", "Hipchat Room Id")
var hcHost = flag.String("hchost", "", "Hipchat Url Base (ex: hipchat.com)")
var roHost = flag.String("rohost", "", "RedOctober Url Base (ex: localhost:8080)")
flag.Parse()
if *vaultPath == "" || *certsPathString == "" || *keysPathString == "" || (*addr == "" && *useSystemdSocket == false) {
@@ -231,7 +239,7 @@ func main() {
certPaths := strings.Split(*certsPathString, ",")
keyPaths := strings.Split(*keysPathString, ",")
if err := core.Init(*vaultPath); err != nil {
if err := core.Init(*vaultPath, *hcKey, *hcRoom, *hcHost, *roHost); err != nil {
log.Fatalf(err.Error())
}
@@ -301,6 +309,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
<li><a href="#encrypt">Encrypt</a></li>
<li><a href="#decrypt">Decrypt</a></li>
<li><a href="#owners">Owners</a></li>
<li><a href="#orders">Order</a></li>
</ul>
</div>
</div>
@@ -438,7 +447,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
<section class="row">
<div id="change-password" class="col-md-6">
<h3>Change password</h3>
<h3>Change account</h3>
<form id="user-change-password" class="ro-user-change-password" role="form" action="/password" method="post">
<div class="feedback change-password-feedback"></div>
@@ -446,16 +455,18 @@ var indexHtml = []byte(`<!DOCTYPE html>
<div class="form-group row">
<div class="col-md-6">
<label for="user-name">User name</label>
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required />
<input type="text" name="Name" class="form-control" id="user-name" placeholder="User name" required/>
</div>
<div class="col-md-6">
<label for="user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password" required />
<input type="password" name="Password" class="form-control" id="user-pass" placeholder="Password"/ required>
</div>
</div>
<div class="form-group">
<label for="user-pass">New password</label>
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New password" required />
<label for="user-pass">New password. Blank for no change.</label>
<input type="password" name="NewPassword" class="form-control" id="user-pass-new" placeholder="New Password"/>
<label for="user-email">Hipchat Name. Blank for no change.</label>
<input type="text" name="HipchatName" class="form-control" id="user-hipchatname" placeholder="New Hipchat Name"/>
</div>
<button type="submit" class="btn btn-primary">Change password</button>
</form>
@@ -583,6 +594,120 @@ var indexHtml = []byte(`<!DOCTYPE html>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="orders" class="col-md-6">
<h3>Create Order</h3>
<form id="order" class="ro-user-order" role="form" action="/order" method="post">
<div class="feedback order-feedback"></div>
<div class="form-group">
<div class="form-group row">
<div class="col-md-6">
<label for="order-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="order-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="order-user-pass">Password</label>
<input type="password" name="Password" class="form-control" id="order-user-pass" placeholder="Password" required />
</div>
<div class="col-md-6">
<label for="order-label">Label</label>
<input type="text" name="Label" class="form-control" id="order-user-label" placeholder="Label" required />
</div>
<div class="col-md-6">
<label for="order-duration">Duration</label>
<input type="text" name="Duration" class="form-control" id="order-duration" placeholder="Duration (e.g., 2h34m)" required />
</div>
<div class="col-md-6">
<label for="order-uses">Uses</label>
<input type="text" name="Uses" class="form-control" id="order-uses" placeholder="Uses" required />
</div>
</div>
<label for="owners-data">Data</label>
<textarea name="Data" class="form-control" id="owners-data" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Create Order</button>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="ordersinfo" class="col-md-6">
<h3>Order Info</h3>
<form id="orderinfo" class="ro-user-order" role="form" action="/orderinfo" method="post">
<div style="overflow-wrap: break-word;" class="feedback orderinfo-feedback"></div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label for="orderinfo-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="orderinfo-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="orderinfo-user-admin">Password</label>
<input type="password" name="Password" class="form-control" id="orderinfo-user-pass" placeholder="Password" required />
</div>
<div class="col-md-6">
<label for="orderinfo-order-num">Order Num</label>
<input type="text" name="OrderNum" class="form-control" id="orderinfo-user-label" placeholder="Order Number" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Order Info</button>
</div>
</form>
</div>
</section>
<hr />
<section class="row">
<div id="ordersout" class="col-md-6">
<h3>Outstanding Orders</h3>
<form id="orderout" class="ro-user-order" role="form" action="/orderout" method="post">
<div style="overflow-wrap: break-word;" class="feedback ordersout-feedback"></div>
<div class="form-group">
<div class="form-group row">
<div class="col-md-6">
<label for="ordersout-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="ordersout-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="ordersout-user-admin">Password</label>
<input type="password" name="Password" class="form-control" id="ordersout-user-pass" placeholder="Password" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Outstanding Orders</button>
</form>
</div>
</section>
<section class="row">
<div id="orderscancel" class="col-md-6">
<h3>Order Cancel</h3>
<form id="ordercancel" class="ro-user-order" role="form" action="/ordercancel" method="post">
<div style="overflow-wrap: break-word;" class="feedback ordercancel-feedback"></div>
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label for="ordercancel-user-admin">User name</label>
<input type="text" name="Name" class="form-control" id="ordercancel-user-admin" placeholder="User name" required />
</div>
<div class="col-md-6">
<label for="ordercancel-user-admin">Password</label>
<input type="password" name="Password" class="form-control" id="ordercancel-user-pass" placeholder="Password" required />
</div>
<div class="col-md-6">
<label for="ordercancel-order-num">Order Num</label>
<input type="text" name="OrderNum" class="form-control" id="ordercancel-user-label" placeholder="Order Number" required />
</div>
</div>
<button type="submit" class="btn btn-primary">Order Cancel</button>
</div>
</form>
</div>
</section>
<hr />
</div>
<footer id="footer" class="footer">
@@ -601,7 +726,6 @@ var indexHtml = []byte(`<!DOCTYPE html>
function submit( $form, options ){
options || (options = {});
$.ajax({
url: $form.attr('action'),
data: JSON.stringify( options.data ),
@@ -638,7 +762,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+data.Name }) );
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Created user: '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -699,7 +823,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+data.Name }) );
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Delegating '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -716,7 +840,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+data.Name }) );
$form.find('.feedback').append( makeAlert({ type: 'success', message: 'Creating '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -730,7 +854,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+data.Name }) );
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Change password for '+htmlspecialchars(data.Name) }) );
}
});
});
@@ -744,7 +868,7 @@ var indexHtml = []byte(`<!DOCTYPE html>
submit( $form, {
data : data,
success : function(d){
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+data.ToModify }) );
$form.find('.feedback').empty().append( makeAlert({ type: 'success', message: 'Successfully modified '+htmlspecialchars(data.ToModify) }) );
}
});
});
@@ -806,6 +930,140 @@ var indexHtml = []byte(`<!DOCTYPE html>
}
});
});
// Create an order
$('body').on('submit', 'form#order', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = JSON.parse(window.atob(d.Response));
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>Order Num: '+d.Num+'</p>' }) );
}
});
});
// Get order info
$('body').on('submit', 'form#orderinfo', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = window.atob(d.Response);
try {
var respData = JSON.parse(d);
var msgText = ""
alert(msgText);
for (var jj in respData) {
if (!jj || jj == "Admins")
continue;
if (!respData.hasOwnProperty(jj)) {
continue;
}
alert(msgText);
msgText += "<p>"+htmlspecialchars(jj)+": "+htmlspecialchars(respData[jj])+"</p>";
}
$form.find('.feedback').empty().append(makeAlert({ type: 'success', message: msgText }));
} catch (e) {
makeAlert({ type: 'failure', message: '<p>Invalid JSON returned</p>' });
}
}
});
});
// Get outstanding order info
$('body').on('submit', 'form#orderout', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = JSON.parse(window.atob(d.Response));
ordout = "";
for (var jj in d){
if (!d.hasOwnProperty(jj))
continue;
var o = d[jj];
ordout += o.Name + " requesting " + o.Label + " has " + o.Delegated + "\n";
}
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>'+ordout+'</p>' }) );
}
});
});
$('body').on('submit', 'form#ordercancel', function(evt){
evt.preventDefault();
var $form = $(evt.currentTarget),
data = serialize($form);
submit( $form, {
data : data,
success : function(d){
d = window.atob(d.Response);
$form.find('.feedback').empty().append(
makeAlert({ type: 'success', message: '<p>'+d+'</p>' }) );
}
});
});
// Init from query string if possible.
var queryParams = document.location.search;
var queryParts = queryParams.split('&');
for (var i=0; i<queryParts.length; i++) {
var part = queryParts[i];
part = part.replace("?", "");
var partPieces = part.split("=");
if (partPieces.length != 2) {
continue;
}
var setValue = null;
var key = partPieces[0];
var value = partPieces[1];
switch (key) {
case "delegator":
setValue = $("#delegate-user");
break;
case "delegatee":
setValue = $("#delegate-users");
break;
case "uses":
setValue = $("#delegate-uses");
break;
case "label":
setValue = $("#delegate-labels");
break;
case "duration":
setValue = $("#delegate-user-time");
break;
case "ordernum":
setValue = $("#delegate-slot");
break;
default:
break;
}
if (setValue) {
setValue.val(value);
}
}
function htmlspecialchars(s) {
if (!isNaN(s)) {
return s;
}
s = s.replace('&', '&amp;');
s = s.replace('<', '&lt;');
s = s.replace('>', '&gt;');
s = s.replace('"', '&quot;');
s = s.replace("'", '&#x27;');
s = s.replace('/', '&#x2F;');
return s
}
});
</script>
</body>