diff --git a/hilink.go b/hilink.go
index 27650d5..1688a02 100644
--- a/hilink.go
+++ b/hilink.go
@@ -2,9 +2,7 @@
package hilink
import (
- "crypto/sha256"
"encoding/base64"
- "encoding/hex"
"errors"
"fmt"
"io/ioutil"
@@ -32,8 +30,43 @@ const (
// TokenHeader is the header used by the WebUI for CSRF tokens.
TokenHeader = "__RequestVerificationToken"
+
+ // TokenHeaderLogin is the header used by the api for session tokens.
+ TokenHeaderLogin = TokenHeader + "one"
)
+// WifiDefaultConfig returns the default configuration of the wireless interface.
+func WifiDefaultConfig() map[string]string {
+ return map[string]string{
+ "Index": "0",
+ "WifiEnable": "0",
+ "WifiSsid": "",
+ "WifiMac": "",
+ "WifiBroadcast": "0",
+ "WifiIsolate": "0",
+ "WifiAuthmode": "WPA2-PSK",
+ "WifiBasicencryptionmodes": "WEP",
+ "WifiWpaencryptionmodes": "AES",
+ "WifiWepKey1": "",
+ "WifiWepKey2": "",
+ "WifiWepKey3": "",
+ "WifiWepKey4": "",
+ "WifiWep128Key1": "",
+ "WifiWep128Key2": "",
+ "WifiWep128Key3": "",
+ "WifiWep128Key4": "",
+ "WifiWepKeyIndex": "1",
+ "WifiWpapsk": "73634297",
+ "MixWifiWpapsk": "73634297",
+ "WifiWpsenbl": "1",
+ "WifiWpscfg": "0",
+ "WifiRotationInterval": "60",
+ "WifiAssociatedStationNum": "0",
+ "wifitotalswitch": "1",
+ "wifiguestofftime": "0",
+ }
+}
+
// Client represents a Hilink client connection.
type Client struct {
rawurl string
@@ -48,6 +81,13 @@ type Client struct {
sync.Mutex
}
+// LoginResponse represents the response message of the login
+// endpoint. Contains the session data.
+type loginResponse struct {
+ tokenID string
+ sessionID string
+}
+
// NewClient creates a new client a Hilink device.
func NewClient(opts ...Option) (*Client, error) {
var err error
@@ -119,7 +159,7 @@ func (c *Client) createRequest(urlstr string, v interface{}) (*http.Request, err
// set content type and CSRF token
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
- req.Header.Set(TokenHeader, c.token)
+ req.Header[TokenHeader] = []string{c.token}
return req, nil
}
@@ -229,6 +269,83 @@ func (c *Client) doReqCheckOK(path string, v interface{}) (bool, error) {
return s == "OK", nil
}
+// doReqLogin sends a request to the server with the provided path. If data is nil,
+// then GET will be used as the HTTP method, otherwise POST will be used. Takes
+// the token number one and the new session id and replaces the current ones.
+func (c *Client) doReqLogin(path string, v interface{}) (*loginResponse, error) {
+ c.Lock()
+ defer c.Unlock()
+
+ var err error
+
+ // create http request
+ q, err := c.createRequest(c.rawurl+path, v)
+ if err != nil {
+ return nil, err
+ }
+
+ // do request
+ r, err := c.client.Do(q)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Body.Close()
+
+ // check status code
+ if r.StatusCode != http.StatusOK {
+ return nil, ErrBadStatusCode
+ }
+
+ // read body
+ body, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ // decode
+ res, err := decodeXML(body, false)
+ if err != nil {
+ return nil, err
+ }
+
+ // expect mxj.Map
+ m, ok := res.(mxj.Map)
+ if !ok {
+ return nil, ErrInvalidResponse
+ }
+
+ // check response present
+ o := map[string]interface{}(m)
+ resp, ok := o["response"]
+ if !ok {
+ return nil, ErrInvalidResponse
+ }
+
+ // convert
+ s, ok := resp.(string)
+ if !ok {
+ return nil, ErrInvalidValue
+ }
+
+ if s != "OK" {
+ return nil, ErrInvalidResponse
+ }
+
+ // retrieve and save new cookie and token
+ var out loginResponse
+
+ // saving token
+ out.tokenID = r.Header.Get(TokenHeaderLogin)
+
+ // saving cookie
+ setcookie := r.Header.Get("Set-Cookie")
+ cookie := strings.Split(setcookie, ";")[0]
+ sessID := strings.TrimPrefix(cookie, "SessionID=")
+ out.sessionID = sessID
+
+ return &out, nil
+}
+
// login authentifies the user using the user identifier and password given
// with the Auth option. Return nil if succeeded, or no Auth option
// was given, or the identifier is an empty string.
@@ -236,14 +353,21 @@ func (c *Client) login() (bool, error) {
if c.authID == "" {
return false, nil
}
- // encode hashed password
- h := sha256.Sum256([]byte(c.authPW + c.token))
- tokenizedPW := base64.RawStdEncoding.EncodeToString([]byte(hex.EncodeToString(h[:])))
- return c.doReqCheckOK("api/user/login", XMLData{
- "Username": c.authID,
- "Password": tokenizedPW,
- "password_type": 4,
- })
+ tokenizedPW := hashPw(c.authID + hashPw(c.authPW) + c.token)
+ resp, err := c.doReqLogin("api/user/login", SimpleRequestXML(
+ "Username", c.authID,
+ "Password", tokenizedPW,
+ "password_type", "4",
+ ))
+ if err != nil {
+ return false, err
+ }
+
+ if err = c.SetSessionAndTokenID(resp.sessionID, resp.tokenID); err != nil {
+ return false, err
+ }
+
+ return true, nil
}
// Do sends a request to the server with the provided path. If data is nil,
@@ -324,6 +448,18 @@ func (c *Client) SetSessionAndTokenID(sessionID, tokenID string) error {
return nil
}
+// ChangePassword changes the current user password
+func (c *Client) ChangePassword(newPassword string) (bool, error) {
+ oldPasswordHash := hashPw(c.authID + hashPw(c.authPW) + c.token)
+ newPasswordHash := base64.StdEncoding.EncodeToString([]byte(newPassword))
+ return c.doReqCheckOK("api/user/password", SimpleRequestXML(
+ "Username", c.authID,
+ "CurrentPassword", oldPasswordHash,
+ "NewPassword", newPasswordHash,
+ "encryption_enable", "1",
+ ))
+}
+
// GlobalConfig retrieves global Hilink configuration.
func (c *Client) GlobalConfig() (XMLData, error) {
return c.Do("config/global/config.xml", nil)
@@ -359,6 +495,23 @@ func (c *Client) WlanConfig() (XMLData, error) {
return c.Do("api/wlan/basic-settings", nil)
}
+// WlanDisable disables the WLAN interface that matches the given ssid.
+func (c *Client) WlanDisable(ssid string, config map[string]string) (bool, error) {
+ // WifiSsid has to be set up before modifying settings
+ var wifiConfig map[string]string
+ if config == nil {
+ wifiConfig = WifiDefaultConfig()
+ } else {
+ wifiConfig = config
+ }
+ wifiConfig["WifiSsid"] = ssid
+
+ return c.doReqCheckOK("api/wlan/multi-basic-settings", SimpleRequestXML(
+ "Ssids", xmlPairsString("", "Ssid", xmlMapString("", wifiConfig)),
+ "WifiRestart", "1",
+ ))
+}
+
// DhcpConfig retrieves DHCP configuration.
func (c *Client) DhcpConfig() (XMLData, error) {
return c.Do("api/dhcp/settings", nil)
@@ -879,8 +1032,30 @@ func (c *Client) UpnpSet(enabled bool) (bool, error) {
))
}
+// IcmpSet enables/disables ICMP.
+func (c *Client) IcmpSet(enabled bool) (bool, error) {
+ return c.doReqCheckOK("api/security/firewall-switch", SimpleRequestXML(
+ "FirewallMainSwitch", "1",
+ "FirewallIPFilterSwitch", "0",
+ "FirewallWanPortPingSwitch", boolToString(enabled),
+ "firewallurlfilterswitch", "0",
+ ))
+}
+
+// LoginSet enables/disables user authentication.
+func (c *Client) LoginSet(enabled bool) (bool, error) {
+ return c.doReqCheckOK("api/user/hilink_login", SimpleRequestXML(
+ "hilink_login", boolToString(enabled),
+ ))
+}
+
+// LoginStatusInfo retrieves the status of user authentication.
+func (c *Client) LoginStatusInfo() (XMLData, error) {
+ return c.Do("api/user/hilink_login", nil)
+}
+
// TODO:
-// UserLogin/UserLogout/UserPasswordChange
+// UserLogout
//
// WLAN management
// firewall ("security") configuration
diff --git a/opts.go b/opts.go
index ac0dbb5..7b6566a 100644
--- a/opts.go
+++ b/opts.go
@@ -29,14 +29,20 @@ func URL(rawurl string) Option {
}
}
+func hashPw(text string) string {
+ h := sha256.New()
+ h.Write([]byte(text))
+ hash := hex.EncodeToString(h.Sum(nil))
+ return base64.StdEncoding.EncodeToString([]byte(hash))
+}
+
// Auth is an option specifying the identifier and password to use.
// The option is ignored if id is an empty string.
func Auth(id, pw string) Option {
return func(c *Client) error {
if id != "" {
c.authID = id
- h := sha256.Sum256([]byte(pw))
- c.authPW = id + base64.StdEncoding.EncodeToString([]byte(hex.EncodeToString(h[:])))
+ c.authPW = pw
}
return nil
}
diff --git a/util.go b/util.go
index cc973b2..eb7e1ae 100644
--- a/util.go
+++ b/util.go
@@ -89,6 +89,16 @@ func xmlPairsString(indent string, vals ...string) string {
return string(xmlPairs(indent, vals...))
}
+// xmlMapString builds a string of XML string map.
+func xmlMapString(indent string, vals map[string]string) string {
+ pairs := make([]string, 0)
+ for k, v := range vals {
+ pairs = append(pairs, k)
+ pairs = append(pairs, v)
+ }
+ return string(xmlPairs(indent, pairs...))
+}
+
// xmlNvp (ie, name value pair) builds a namevalue XML pair.
func xmlNvp(name, value string) string {
return xmlPairsString("", "Name", name, "Value", value)