feat: inital cwmp server

This commit is contained in:
leandrofars 2024-04-18 17:00:48 -03:00
parent a430857ce3
commit 558088d74b
17 changed files with 1314 additions and 3 deletions

View File

1
backend/services/acs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.local

View File

@ -0,0 +1,97 @@
# Moses ACS [![Build Status](https://travis-ci.org/lucacervasio/mosesacs.svg?branch=master)](https://travis-ci.org/lucacervasio/mosesacs)
An ACS in Go for provisioning CPEs, suitable for test purposes or production deployment.
## Getting started
Install the package:
go get oktopUSP/backend/services/acs
Run daemon:
mosesacs -d
Connect to it and get a cli:
mosesacs
Congratulations, you've connected to the daemon via websocket. Now you can issue commands via CLI or browse the embedded webserver at http://localhost:9292/www
## Compatibility on ARM
Moses is built on purpose only with dependencies in pure GO. So it runs on ARM processors with no issues. We tested it on QNAP devices and Raspberry for remote control.
## CLI commands
### 1. `list`: list CPEs
example:
```
moses@localhost:9292/> list
cpe list
CPE A54FD with OUI 006754
```
### 2. `readMib SERIAL LEAF/SUBTREE`: read a specific leaf or a subtree
example:
```
moses@localhost:9292/> readMib A54FD Device.
Received an Inform from [::1]:58582 (3191 bytes) with SerialNumber A54FD and EventCodes 6 CONNECTION REQUEST
InternetGatewayDevice.Time.NTPServer1 : pool.ntp.org
InternetGatewayDevice.Time.CurrentLocalTime : 2014-07-11T09:08:25
InternetGatewayDevice.Time.LocalTimeZone : +00:00
InternetGatewayDevice.Time.LocalTimeZoneName : Greenwich Mean Time : Dublin
InternetGatewayDevice.Time.DaylightSavingsUsed : 0
```
### 3. `writeMib SERIAL LEAF VALUE`: issue a SetParameterValues and write a value into a leaf
example:
```
moses@localhost:9292/> writeMib A54FD InternetGatewayDevice.Time.Enable false
Received an Inform from [::1]:58582 (3191 bytes) with SerialNumber A54FD and EventCodes 6 CONNECTION REQUEST
```
### 4. `GetParameterNames SERIAL LEAF/SUBTREE`: issue a GetParameterNames and get all leaves/objects at first level
example:
```
moses@localhost:9292/> GetParameterNames A54FD InternetGatewayDevice.
Received an Inform from [::1]:55385 (3119 bytes) with SerialNumber A54FD and EventCodes 6 CONNECTION REQUEST
InternetGatewayDevice.LANDeviceNumberOfEntries : 0
InternetGatewayDevice.WANDeviceNumberOfEntries : 0
InternetGatewayDevice.DeviceInfo. : 0
InternetGatewayDevice.ManagementServer. : 0
InternetGatewayDevice.Time. : 0
InternetGatewayDevice.Layer3Forwarding. : 0
InternetGatewayDevice.LANDevice. : 0
InternetGatewayDevice.WANDevice. : 0
InternetGatewayDevice.X_00507F_InternetAcc. : 0
InternetGatewayDevice.X_00507F_LAN. : 0
InternetGatewayDevice.X_00507F_NAT. : 0
InternetGatewayDevice.X_00507F_VLAN. : 0
InternetGatewayDevice.X_00507F_Firewall. : 0
InternetGatewayDevice.X_00507F_Applications. : 0
InternetGatewayDevice.X_00507F_System. : 0
InternetGatewayDevice.X_00507F_Status. : 0
InternetGatewayDevice.X_00507F_Diagnostics. : 0
```
## Services exposed
Moses exposes three services:
- http://localhost:9292/acs is the endpoint for the CPEs to connect
- http://localhost:9292/www is the embedded webserver to control your CPEs
- ws://localhost:9292/ws is the websocket endpoint used by the cli to issue commands. Read about the API specification if you want to build a custom frontend which interacts with mosesacs daemon.

View File

@ -0,0 +1,19 @@
package main
import (
"oktopUSP/backend/services/acs/internal/config"
"oktopUSP/backend/services/acs/internal/nats"
"oktopUSP/backend/services/acs/internal/server"
"oktopUSP/backend/services/acs/internal/server/handler"
)
func main() {
c := config.NewConfig()
natsActions := nats.StartNatsClient(c.Nats)
h := handler.NewHandler(natsActions.Publish, natsActions.Subscribe)
server.Run(c.Acs, natsActions, h)
}

View File

@ -0,0 +1,19 @@
module oktopUSP/backend/services/acs
go 1.22.2
require (
github.com/joho/godotenv v1.5.1
github.com/nats-io/nats.go v1.34.1
github.com/oleiade/lane v1.0.1
golang.org/x/net v0.24.0
)
require (
github.com/klauspost/compress v1.17.2 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

View File

@ -0,0 +1,20 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4=
github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oleiade/lane v1.0.1 h1:hXofkn7GEOubzTwNpeL9MaNy8WxolCYb9cInAIeqShU=
github.com/oleiade/lane v1.0.1/go.mod h1:IyTkraa4maLfjq/GmHR+Dxb4kCMtEGeb+qmhlrQ5Mk4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

View File

@ -0,0 +1,113 @@
package auth
import (
"crypto/md5"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type myjar struct {
jar map[string][]*http.Cookie
}
func (p *myjar) SetCookies(u *url.URL, cookies []*http.Cookie) {
p.jar[u.Host] = cookies
}
func (p *myjar) Cookies(u *url.URL) []*http.Cookie {
return p.jar[u.Host]
}
func Auth(username string, password string, uri string) (bool, error) {
client := &http.Client{}
jar := &myjar{}
jar.jar = make(map[string][]*http.Cookie)
client.Jar = jar
var req *http.Request
var resp *http.Response
var err error
req, err = http.NewRequest("GET", uri, nil)
resp, err = client.Do(req)
if err != nil {
return false, err
}
if resp.StatusCode == 401 {
var authorization map[string]string = DigestAuthParams(resp)
realmHeader := authorization["realm"]
qopHeader := authorization["qop"]
nonceHeader := authorization["nonce"]
opaqueHeader := authorization["opaque"]
realm := realmHeader
// A1
h := md5.New()
A1 := fmt.Sprintf("%s:%s:%s", username, realm, password)
io.WriteString(h, A1)
HA1 := fmt.Sprintf("%x", h.Sum(nil))
// A2
h = md5.New()
A2 := fmt.Sprintf("GET:%s", "/auth")
io.WriteString(h, A2)
HA2 := fmt.Sprintf("%x", h.Sum(nil))
// response
cnonce := RandomKey()
response := H(strings.Join([]string{HA1, nonceHeader, "00000001", cnonce, qopHeader, HA2}, ":"))
// now make header
AuthHeader := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc=00000001, qop=%s, response="%s", opaque="%s", algorithm=MD5`,
username, realmHeader, nonceHeader, "/auth", cnonce, qopHeader, response, opaqueHeader)
req.Header.Set("Authorization", AuthHeader)
resp, err = client.Do(req)
} else {
return false, fmt.Errorf("response status code should have been 401, it was %v", resp.StatusCode)
}
return resp.StatusCode == 200, err
}
/*
Parse Authorization header from the http.Request. Returns a map of
auth parameters or nil if the header is not a valid parsable Digest
auth header.
*/
func DigestAuthParams(r *http.Response) map[string]string {
s := strings.SplitN(r.Header.Get("Www-Authenticate"), " ", 2)
if len(s) != 2 || s[0] != "Digest" {
return nil
}
result := map[string]string{}
for _, kv := range strings.Split(s[1], ",") {
parts := strings.SplitN(kv, "=", 2)
if len(parts) != 2 {
continue
}
result[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ")
}
return result
}
func RandomKey() string {
k := make([]byte, 12)
for bytes := 0; bytes < len(k); {
n, err := rand.Read(k[bytes:])
if err != nil {
panic("rand.Read() failed")
}
bytes += n
}
return base64.StdEncoding.EncodeToString(k)
}
/*
H function for MD5 algorithm (returns a lower-case hex MD5 digest)
*/
func H(data string) string {
digest := md5.New()
digest.Write([]byte(data))
return fmt.Sprintf("%x", digest.Sum(nil))
}

View File

@ -0,0 +1,111 @@
package config
import (
"context"
"flag"
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
const LOCAL_ENV = ".env.local"
type Nats struct {
Url string
Name string
VerifyCertificates bool
Ctx context.Context
}
type Acs struct {
Port string
Tls bool
TlsPort bool
NoTls bool
Username string
Password string
Route string
}
type Config struct {
Acs Acs
Nats Nats
}
func NewConfig() *Config {
loadEnvVariables()
log.SetFlags(log.LstdFlags | log.Lshortfile)
natsUrl := flag.String("nats_url", lookupEnvOrString("NATS_URL", "nats://localhost:4222"), "url for nats server")
natsName := flag.String("nats_name", lookupEnvOrString("NATS_NAME", "adapter"), "name for nats client")
natsVerifyCertificates := flag.Bool("nats_verify_certificates", lookupEnvOrBool("NATS_VERIFY_CERTIFICATES", false), "verify validity of certificates from nats server")
acsPort := flag.String("acs_port", lookupEnvOrString("ACS_PORT", ":9292"), "port for acs server")
acsRoute := flag.String("acs_route", lookupEnvOrString("ACS_ROUTE", "/acs"), "route for acs server")
flHelp := flag.Bool("help", false, "Help")
/*
App variables priority:
1º - Flag through command line.
2º - Env variables.
3º - Default flag value.
*/
flag.Parse()
if *flHelp {
flag.Usage()
os.Exit(0)
}
ctx := context.TODO()
return &Config{
Nats: Nats{
Url: *natsUrl,
Name: *natsName,
VerifyCertificates: *natsVerifyCertificates,
Ctx: ctx,
},
Acs: Acs{
Port: *acsPort,
Route: *acsRoute,
},
}
}
func loadEnvVariables() {
err := godotenv.Load()
if _, err := os.Stat(LOCAL_ENV); err == nil {
_ = godotenv.Overload(LOCAL_ENV)
log.Printf("Loaded variables from '%s'", LOCAL_ENV)
return
}
if err != nil {
log.Println("Error to load environment variables:", err)
} else {
log.Println("Loaded variables from '.env'")
}
}
func lookupEnvOrString(key string, defaultVal string) string {
if val, _ := os.LookupEnv(key); val != "" {
return val
}
return defaultVal
}
func lookupEnvOrBool(key string, defaultVal bool) bool {
if val, _ := os.LookupEnv(key); val != "" {
v, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("LookupEnvOrBool[%s]: %v", key, err)
}
return v
}
return defaultVal
}

View File

@ -0,0 +1,541 @@
package cwmp
import (
"crypto/rand"
"encoding/xml"
"fmt"
"strconv"
"strings"
"time"
)
type SoapEnvelope struct {
XMLName xml.Name
Header SoapHeader
Body SoapBody
}
type SoapHeader struct {
Id string `xml:"ID"`
}
type SoapBody struct {
CWMPMessage CWMPMessage `xml:",any"`
}
type CWMPMessage struct {
XMLName xml.Name
}
type EventStruct struct {
EventCode string
CommandKey string
}
type ParameterValueStruct struct {
Name string
Value string
}
type ParameterInfoStruct struct {
Name string
Writable string
}
type SetParameterValues_ struct {
ParameterList []ParameterValueStruct `xml:"Body>SetParameterValues>ParameterList>ParameterValueStruct"`
ParameterKey string `xml:"Body>SetParameterValues>ParameterKey>string"`
}
type GetParameterValues_ struct {
ParameterNames []string `xml:"Body>GetParameterValues>ParameterNames>string"`
}
type GetParameterNames_ struct {
ParameterPath []string `xml:"Body>GetParameterNames>ParameterPath"`
NextLevel string `xml:"Body>GetParameterNames>NextLevel"`
}
type GetParameterValuesResponse struct {
ParameterList []ParameterValueStruct `xml:"Body>GetParameterValuesResponse>ParameterList>ParameterValueStruct"`
}
type GetParameterNamesResponse struct {
ParameterList []ParameterInfoStruct `xml:"Body>GetParameterNamesResponse>ParameterList>ParameterInfoStruct"`
}
type CWMPInform struct {
DeviceId DeviceID `xml:"Body>Inform>DeviceId"`
Events []EventStruct `xml:"Body>Inform>Event>EventStruct"`
ParameterList []ParameterValueStruct `xml:"Body>Inform>ParameterList>ParameterValueStruct"`
}
func (s *SoapEnvelope) KindOf() string {
return s.Body.CWMPMessage.XMLName.Local
}
func (i *CWMPInform) GetEvents() string {
res := ""
for idx := range i.Events {
res += i.Events[idx].EventCode
}
return res
}
func (i *CWMPInform) GetConnectionRequest() string {
for idx := range i.ParameterList {
// valid condition for both tr98 and tr181
if strings.HasSuffix(i.ParameterList[idx].Name, "Device.ManagementServer.ConnectionRequestURL") {
return i.ParameterList[idx].Value
}
}
return ""
}
func (i *CWMPInform) GetSoftwareVersion() string {
for idx := range i.ParameterList {
if strings.HasSuffix(i.ParameterList[idx].Name, "Device.DeviceInfo.SoftwareVersion") {
return i.ParameterList[idx].Value
}
}
return ""
}
func (i *CWMPInform) GetHardwareVersion() string {
for idx := range i.ParameterList {
if strings.HasSuffix(i.ParameterList[idx].Name, "Device.DeviceInfo.HardwareVersion") {
return i.ParameterList[idx].Value
}
}
return ""
}
func (i *CWMPInform) GetDataModelType() string {
if strings.HasPrefix(i.ParameterList[0].Name, "InternetGatewayDevice") {
return "TR098"
} else if strings.HasPrefix(i.ParameterList[0].Name, "Device") {
return "TR181"
}
return ""
}
type DeviceID struct {
Manufacturer string
OUI string
SerialNumber string
}
func InformResponse(mustUnderstand string) string {
mustUnderstandHeader := ""
if mustUnderstand != "" {
mustUnderstandHeader = `<cwmp:ID soap:mustUnderstand="1">` + mustUnderstand + `</cwmp:ID>`
}
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header>` + mustUnderstandHeader + `</soap:Header>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:InformResponse>
<MaxEnvelopes>1</MaxEnvelopes>
</cwmp:InformResponse>
</soap:Body>
</soap:Envelope>`
}
func GetParameterValues(leaf string) string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:GetParameterValues>
<ParameterNames>
<string>` + leaf + `</string>
</ParameterNames>
</cwmp:GetParameterValues>
</soap:Body>
</soap:Envelope>`
}
func GetParameterMultiValues(leaves []string) string {
msg := `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:GetParameterValues>
<ParameterNames>`
for idx := range leaves {
msg += `<string>` + leaves[idx] + `</string>`
}
msg += `</ParameterNames>
</cwmp:GetParameterValues>
</soap:Body>
</soap:Envelope>`
return msg
}
func SetParameterValues(leaf string, value string) string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:SetParameterValues>
<ParameterList soapenc:arrayType="cwmp:ParameterValueStruct[1]">
<ParameterValueStruct>
<Name>` + leaf + `</Name>
<Value>` + value + `</Value>
</ParameterValueStruct>
</ParameterList>
<ParameterKey>LC1309` + randToken() + `</ParameterKey>
</cwmp:SetParameterValues>
</soap:Body>
</soap:Envelope>`
}
func randToken() string {
b := make([]byte, 8)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
func SetParameterMultiValues(data map[string]string) string {
msg := `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:SetParameterValues>
<ParameterList soapenc:arrayType="cwmp:ParameterValueStruct[` + string(len(data)) + `]">`
for key, value := range data {
msg += `<ParameterValueStruct>
<Name>` + key + `</Name>
<Value>` + value + `</Value>
</ParameterValueStruct>`
}
msg += `</ParameterList>
<ParameterKey>LC1309` + randToken() + `</ParameterKey>
</cwmp:SetParameterValues>
</soap:Body>
</soap:Envelope>`
return msg
}
func GetParameterNames(leaf string, nextlevel int) string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:GetParameterNames>
<ParameterPath>` + leaf + `</ParameterPath>
<NextLevel>` + strconv.Itoa(nextlevel) + `</NextLevel>
</cwmp:GetParameterNames>
</soap:Body>
</soap:Envelope>`
}
func FactoryReset() string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:FactoryReset/>
</soap:Body>
</soap:Envelope>`
}
func Download(filetype, url, username, password, filesize string) string {
// 3 Vendor Configuration File
// 1 Firmware Upgrade Image
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:Download>
<CommandKey>MSDWK</CommandKey>
<FileType>` + filetype + `</FileType>
<URL>` + url + `</URL>
<Username>` + username + `</Username>
<Password>` + password + `</Password>
<FileSize>` + filesize + `</FileSize>
<TargetFileName></TargetFileName>
<DelaySeconds>0</DelaySeconds>
<SuccessURL></SuccessURL>
<FailureURL></FailureURL>
</cwmp:Download>
</soap:Body>
</soap:Envelope>`
}
func CancelTransfer() string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:CancelTransfer>
<CommandKey></CommandKey>
<cwmp:CancelTransfer/>
</soap:Body>
</soap:Envelope>`
}
type TimeWindowStruct struct {
WindowStart string
WindowEnd string
WindowMode string
UserMessage string
MaxRetries string
}
func (window *TimeWindowStruct) String() string {
return `<TimeWindowStruct>
<WindowStart>` + window.WindowStart + `</WindowStart>
<WindowEnd>` + window.WindowEnd + `</WindowEnd>
<WindowMode>` + window.WindowMode + `</WindowMode>
<UserMessage>` + window.UserMessage + `</UserMessage>
<MaxRetries>` + window.MaxRetries + `</MaxRetries>
</TimeWindowStruct>`
}
func ScheduleDownload(filetype, url, username, password, filesize string, windowslist []fmt.Stringer) string {
ret := `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cwmp:ScheduleDownload>
<CommandKey>MSDWK</CommandKey>
<FileType>` + filetype + `</FileType>
<URL>` + url + `</URL>
<Username>` + username + `</Username>
<Password>` + password + `</Password>
<FileSize>` + filesize + `</FileSize>
<TargetFileName></TargetFileName>
<TimeWindowList>`
for _, op := range windowslist {
ret += op.String()
}
ret += `</TimeWindowList>
</cwmp:ScheduleDownload>
</soap:Body>
</soap:Envelope>`
return ret
}
type InstallOpStruct struct {
Url string
Uuid string
Username string
Password string
ExecutionEnvironment string
}
func (op *InstallOpStruct) String() string {
return `<InstallOpStruct>
<URL>` + op.Url + `</URL>
<UUID>` + op.Uuid + `</UUID>
<Username>` + op.Username + `</Username>
<Password>` + op.Password + `</Password>
<ExecutionEnvRef>` + op.ExecutionEnvironment + `</ExecutionEnvRef>
</InstallOpStruct>`
}
type UpdateOpStruct struct {
Uuid string
Version string
Url string
Username string
Password string
}
func (op *UpdateOpStruct) String() string {
return `<UpdateOpStruct>
<UUID>` + op.Uuid + `</UUID>
<Version>` + op.Version + `</Version>
<URL>` + op.Url + `</URL>
<Username>` + op.Username + `</Username>
<Password>` + op.Password + `</Password>
</UpdateOpStruct>`
}
type UninstallOpStruct struct {
Uuid string
Version string
ExecutionEnvironment string
}
func (op *UninstallOpStruct) String() string {
return `<UninstallOpStruct>
<UUID>` + op.Uuid + `</UUID>
<Version>` + op.Version + `</Version>
<ExecutionEnvRef>` + op.ExecutionEnvironment + `</ExecutionEnvRef>
</UninstallOpStruct>`
}
func ChangeDuState(ops []fmt.Stringer) string {
ret := `<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:schemaLocation="urn:dslforum-org:cwmp-1-0 ..\schemas\wt121.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Header/>
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<cmwp:ChangeDUState>
<Operations>`
for _, op := range ops {
ret += op.String()
}
ret += `</Operations>
<CommandKey></CommandKey>
</cmwp:ChangeDUState>
</soap:Body>
</soap:Envelope>`
return ret
}
// CPE side
func Inform(serial string) string {
return `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0"><soap:Header><cwmp:ID soap:mustUnderstand="1">5058</cwmp:ID></soap:Header>
<soap:Body><cwmp:Inform><DeviceId><Manufacturer>ADB Broadband</Manufacturer>
<OUI>0013C8</OUI>
<ProductClass>VV5522</ProductClass>
<SerialNumber>` + serial + `</SerialNumber>
</DeviceId>
<Event soap-enc:arrayType="cwmp:EventStruct[1]">
<EventStruct><EventCode>6 CONNECTION REQUEST</EventCode>
<CommandKey></CommandKey>
</EventStruct>
</Event>
<MaxEnvelopes>1</MaxEnvelopes>
<CurrentTime>` + time.Now().Format(time.RFC3339) + `</CurrentTime>
<RetryCount>0</RetryCount>
<ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[8]">
<ParameterValueStruct><Name>InternetGatewayDevice.ManagementServer.ConnectionRequestURL</Name>
<Value xsi:type="xsd:string">http://104.199.175.27:7547/ConnectionRequest-` + serial + `</Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.ManagementServer.ParameterKey</Name>
<Value xsi:type="xsd:string"></Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.DeviceSummary</Name>
<Value xsi:type="xsd:string">InternetGatewayDevice:1.2[](Baseline:1,EthernetLAN:1,WiFiLAN:1,ADSLWAN:1,EthernetWAN:1,QoS:1,QoSDynamicFlow:1,Bridging:1,Time:1,IPPing:1,TraceRoute:1,DeviceAssociation:1,UDPConnReq:1),VoiceService:1.0[1](TAEndpoint:1,SIPEndpoint:1)</Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.DeviceInfo.HardwareVersion</Name>
<Value xsi:type="xsd:string">` + serial + `</Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.DeviceInfo.ProvisioningCode</Name>
<Value xsi:type="xsd:string">ABCD</Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.DeviceInfo.SoftwareVersion</Name>
<Value xsi:type="xsd:string">4.0.8.17785</Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.DeviceInfo.SpecVersion</Name>
<Value xsi:type="xsd:string">1.0</Value>
</ParameterValueStruct>
<ParameterValueStruct><Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress</Name>
<Value xsi:type="xsd:string">12.0.0.10</Value>
</ParameterValueStruct>
</ParameterList>
</cwmp:Inform>
</soap:Body></soap:Envelope>`
}
/*
func BuildGetParameterValuesResponse(serial string, leaves GetParameterValues_) string {
ret := `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
<soap:Header><cwmp:ID soap:mustUnderstand="1">3</cwmp:ID></soap:Header>
<soap:Body><cwmp:GetParameterValuesResponse>`
db, _ := sqlite3.Open("/tmp/cpe.db")
n_leaves := 0
var temp string
for _, leaf := range leaves.ParameterNames {
sql := "select key, value, tipo from params where key like '" + leaf + "%'"
for s, err := db.Query(sql); err == nil; err = s.Next() {
n_leaves++
var key string
var value string
var tipo string
s.Scan(&key, &value, &tipo)
temp += `<ParameterValueStruct>
<Name>` + key + `</Name>
<Value xsi:type="` + tipo + `">` + value + `</Value>
</ParameterValueStruct>`
}
}
ret += `<ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[` + strconv.Itoa(n_leaves) + `]">`
ret += temp
ret += `</ParameterList></cwmp:GetParameterValuesResponse></soap:Body></soap:Envelope>`
return ret
}
func BuildGetParameterNamesResponse(serial string, leaves GetParameterNames_) string {
ret := `<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
<soap:Header><cwmp:ID soap:mustUnderstand="1">69</cwmp:ID></soap:Header>
<soap:Body><cwmp:GetParameterNamesResponse>`
db, _ := sqlite3.Open("/tmp/cpe.db")
obj := make(map[string]bool)
var temp string
for _, leaf := range leaves.ParameterPath {
fmt.Println(leaf)
sql := "select key, value, tipo from params where key like '" + leaf + "%'"
for s, err := db.Query(sql); err == nil; err = s.Next() {
var key string
var value string
var tipo string
s.Scan(&key, &value, &tipo)
var sp = strings.Split(strings.Split(key, leaf)[1], ".")
nextlevel, _ := strconv.Atoi(leaves.NextLevel)
if nextlevel == 0 {
root := leaf
obj[root] = true
for idx := range sp {
if idx == len(sp)-1 {
root = root + sp[idx]
} else {
root = root + sp[idx] + "."
}
obj[root] = true
}
} else {
if !obj[sp[0]] {
if len(sp) > 1 {
obj[leaf+sp[0]+"."] = true
} else {
obj[leaf+sp[0]] = true
}
}
}
}
}
for o := range obj {
temp += `<ParameterInfoStruct>
<Name>` + o + `</Name>
<Writable>true</Writable>
</ParameterInfoStruct>`
}
fmt.Println(len(obj))
ret += `<ParameterList soap-enc:arrayType="cwmp:ParameterInfoStruct[]">`
ret += temp
ret += `</ParameterList></cwmp:GetParameterNamesResponse></soap:Body></soap:Envelope>`
return ret
}
*/

View File

@ -0,0 +1,149 @@
package nats
import (
"context"
"errors"
"log"
"oktopUSP/backend/services/acs/internal/config"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
const (
CWMP_STREAM_NAME = "cwmp"
)
type NatsActions struct {
Publish func(string, []byte) error
Subscribe func(string, func(*nats.Msg)) error
}
func StartNatsClient(c config.Nats) NatsActions {
var (
nc *nats.Conn
err error
)
opts := defineOptions(c)
log.Printf("Connecting to NATS server %s", c.Url)
for {
nc, err = nats.Connect(c.Url, opts...)
if err != nil {
time.Sleep(5 * time.Second)
continue
}
break
}
log.Printf("Successfully connected to NATS server %s", c.Url)
js, err := jetstream.New(nc)
if err != nil {
log.Fatalf("Failed to create JetStream client: %v", err)
}
streams := defineStreams()
err = createStreams(c.Ctx, js, streams)
if err != nil {
log.Fatalf("Failed to create Consumer: %v", err)
}
consumers := defineConsumers()
err = createConsumers(c.Ctx, js, consumers)
if err != nil {
log.Fatalf("Failed to create Consumer: %v", err)
}
return NatsActions{
Publish: publisher(js),
Subscribe: subscriber(nc),
}
}
func subscriber(nc *nats.Conn) func(string, func(*nats.Msg)) error {
return func(subject string, handler func(*nats.Msg)) error {
_, err := nc.Subscribe(subject, handler)
if err != nil {
log.Printf("error to subscribe to subject %s error: %q", subject, err)
}
return err
}
}
func publisher(js jetstream.JetStream) func(string, []byte) error {
return func(subject string, payload []byte) error {
_, err := js.PublishAsync(subject, payload)
if err != nil {
log.Printf("error to send jetstream message: %q", err)
}
return err
}
}
func createStreams(ctx context.Context, js jetstream.JetStream, streams []string) error {
for _, stream := range streams {
_, err := js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
Name: stream,
Description: "Stream for " + stream + " messages",
Subjects: []string{stream + ".>"},
Retention: jetstream.InterestPolicy,
})
if err != nil {
return errors.New(err.Error() + " | consumer:" + stream)
}
}
return nil
}
func createConsumers(ctx context.Context, js jetstream.JetStream, consumers []string) error {
for _, consumer := range consumers {
_, err := js.CreateOrUpdateConsumer(ctx, consumer, jetstream.ConsumerConfig{
Name: consumer,
Description: "Consumer for " + consumer + " messages",
AckPolicy: jetstream.AckExplicitPolicy,
Durable: consumer,
})
if err != nil {
return err
}
}
return nil
}
func defineStreams() []string {
return []string{
CWMP_STREAM_NAME,
}
}
func defineConsumers() []string {
return []string{
CWMP_STREAM_NAME,
}
}
func defineOptions(c config.Nats) []nats.Option {
var opts []nats.Option
opts = append(opts, nats.Name(c.Name))
opts = append(opts, nats.MaxReconnects(-1))
opts = append(opts, nats.ReconnectWait(5*time.Second))
opts = append(opts, nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
log.Printf("Got disconnected! Reason: %q\n", err)
}))
opts = append(opts, nats.ReconnectHandler(func(nc *nats.Conn) {
log.Printf("Got reconnected to %v!\n", nc.ConnectedUrl())
}))
opts = append(opts, nats.ClosedHandler(func(nc *nats.Conn) {
log.Printf("Connection closed. Reason: %q\n", nc.LastError())
}))
if c.VerifyCertificates {
opts = append(opts, nats.RootCAs())
}
return opts
}

View File

@ -0,0 +1,215 @@
package handler
import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"log"
"net/http"
"oktopUSP/backend/services/acs/internal/cwmp"
"time"
"github.com/nats-io/nats.go"
"github.com/oleiade/lane"
"golang.org/x/net/websocket"
)
const Version = "1.0.0"
type Request struct {
Id string
Websocket *websocket.Conn
CwmpMessage string
Callback func(msg *WsSendMessage) error
}
type CPE struct {
SerialNumber string
Manufacturer string
OUI string
ConnectionRequestURL string
XmppId string
XmppUsername string
XmppPassword string
SoftwareVersion string
ExternalIPAddress string
State string
Queue *lane.Queue
Waiting *Request
HardwareVersion string
LastConnection time.Time
DataModel string
KeepConnectionOpen bool
}
type Message struct {
SerialNumber string
Message string
}
type WsMessage struct {
Cmd string
}
type WsSendMessage struct {
MsgType string
Data json.RawMessage
}
type MsgCPEs struct {
CPES map[string]CPE
}
type Handler struct {
pub func(string, []byte) error
sub func(string, func(*nats.Msg)) error
cpes map[string]CPE
}
func NewHandler(pub func(string, []byte) error, sub func(string, func(*nats.Msg)) error) *Handler {
return &Handler{
pub: pub,
sub: sub,
cpes: make(map[string]CPE),
}
}
func (h *Handler) CwmpHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("--> Connection from %s", r.RemoteAddr)
defer r.Body.Close()
defer log.Printf("<-- Connection from %s closed", r.RemoteAddr)
tmp, _ := ioutil.ReadAll(r.Body)
body := string(tmp)
len := len(body)
log.Printf("body:\n %v", body)
var envelope cwmp.SoapEnvelope
xml.Unmarshal(tmp, &envelope)
messageType := envelope.Body.CWMPMessage.XMLName.Local
var cpe *CPE
w.Header().Set("Server", "Oktopus "+Version)
if messageType != "Inform" {
if cookie, err := r.Cookie("oktopus"); err == nil {
if _, exists := h.cpes[cookie.Value]; !exists {
log.Printf("CPE with serial number %s not found", cookie.Value)
}
} else {
fmt.Println("cookie 'oktopus' missing")
w.WriteHeader(401)
return
}
}
if messageType == "Inform" {
var Inform cwmp.CWMPInform
xml.Unmarshal(tmp, &Inform)
var addr string
if r.Header.Get("X-Real-Ip") != "" {
addr = r.Header.Get("X-Real-Ip")
} else {
addr = r.RemoteAddr
}
sn := Inform.DeviceId.SerialNumber
if _, exists := h.cpes[sn]; !exists {
fmt.Println("New device: " + sn)
h.cpes[sn] = CPE{
SerialNumber: sn,
LastConnection: time.Now().UTC(),
SoftwareVersion: Inform.GetSoftwareVersion(),
HardwareVersion: Inform.GetHardwareVersion(),
ExternalIPAddress: addr,
ConnectionRequestURL: Inform.GetConnectionRequest(),
OUI: Inform.DeviceId.OUI,
Queue: lane.NewQueue(),
DataModel: Inform.GetDataModelType(),
KeepConnectionOpen: false}
}
obj := h.cpes[sn]
cpe := &obj
cpe.LastConnection = time.Now().UTC()
log.Printf("Received an Inform from %s (%d bytes) with SerialNumber %s and EventCodes %s", addr, len, sn, Inform.GetEvents())
expiration := time.Now().AddDate(0, 0, 1)
cookie := http.Cookie{Name: "oktopus", Value: sn, Expires: expiration}
http.SetCookie(w, &cookie)
} else if messageType == "TransferComplete" {
} else if messageType == "GetRPC" {
} else {
if len == 0 {
log.Printf("Got Empty Post")
}
if cpe.Waiting != nil {
var e cwmp.SoapEnvelope
xml.Unmarshal([]byte(body), &e)
if e.KindOf() == "GetParameterNamesResponse" {
var envelope cwmp.GetParameterNamesResponse
xml.Unmarshal([]byte(body), &envelope)
msg := new(WsSendMessage)
msg.MsgType = "GetParameterNamesResponse"
msg.Data, _ = json.Marshal(envelope)
cpe.Waiting.Callback(msg)
// if err := websocket.JSON.Send(cpe.Waiting.Websocket, msg); err != nil {
// fmt.Println("error while sending back answer:", err)
// }
} else if e.KindOf() == "GetParameterValuesResponse" {
var envelope cwmp.GetParameterValuesResponse
xml.Unmarshal([]byte(body), &envelope)
msg := new(WsSendMessage)
msg.MsgType = "GetParameterValuesResponse"
msg.Data, _ = json.Marshal(envelope)
cpe.Waiting.Callback(msg)
// if err := websocket.JSON.Send(cpe.Waiting.Websocket, msg); err != nil {
// fmt.Println("error while sending back answer:", err)
// }
} else {
msg := new(WsMessage)
msg.Cmd = body
if err := websocket.JSON.Send(cpe.Waiting.Websocket, msg); err != nil {
fmt.Println("error while sending back answer:", err)
}
}
cpe.Waiting = nil
}
// Got Empty Post or a Response. Now check for any event to send, otherwise 204
if cpe.Queue.Size() > 0 {
req := cpe.Queue.Dequeue().(Request)
// fmt.Println("sending "+req.CwmpMessage)
fmt.Fprintf(w, req.CwmpMessage)
cpe.Waiting = &req
} else {
if cpe.KeepConnectionOpen {
fmt.Println("I'm keeping connection open")
} else {
w.WriteHeader(204)
}
}
}
}

View File

@ -0,0 +1,23 @@
package server
import (
"log"
"net/http"
"oktopUSP/backend/services/acs/internal/config"
"oktopUSP/backend/services/acs/internal/nats"
"oktopUSP/backend/services/acs/internal/server/handler"
"os"
)
func Run(c config.Acs, natsActions nats.NatsActions, h *handler.Handler) {
http.HandleFunc(c.Route, h.CwmpHandler)
log.Printf("ACS running at %s%s", c.Port, c.Route)
err := http.ListenAndServe(c.Port, nil)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
}

View File

@ -1,2 +0,0 @@
MONGO_URI="mongodb://localhost:27017"
CONTROLLER_PASSWORD="test123"

View File

@ -0,0 +1 @@
*.local

View File

@ -1,2 +1,3 @@
REDIS_ENABLE=false REDIS_ENABLE=false
REDIS_ADDR=redis_usp:6379 REDIS_ADDR=redis_usp:6379
NATS_URL=nats://msg_broker:4222

1
deploy/compose/.env.ws Normal file
View File

@ -0,0 +1 @@
NATS_URL=nats://msg_broker:4222

View File

@ -66,6 +66,8 @@ services:
container_name: websockets container_name: websockets
ports: ports:
- 8080:8080 - 8080:8080
env_file:
- .env.ws
networks: networks:
usp_network: usp_network:
ipv4_address: 172.16.235.7 ipv4_address: 172.16.235.7