Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 187 additions & 12 deletions hilink.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
package hilink

import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -229,21 +269,105 @@ 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.
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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Name>name</Name><Value>value</Value> XML pair.
func xmlNvp(name, value string) string {
return xmlPairsString("", "Name", name, "Value", value)
Expand Down