Merge pull request #648 from jbenet/nat-lite

NAT-Lite
This commit is contained in:
Juan Batiz-Benet 2015-01-26 16:59:33 -08:00
commit ae5e244354
40 changed files with 13992 additions and 29 deletions

14
Godeps/Godeps.json generated
View File

@ -92,6 +92,10 @@
"ImportPath": "github.com/facebookgo/stackerr",
"Rev": "060fbf9364c89acd41bf710e9e92915a90e7a5b5"
},
{
"ImportPath": "github.com/fd/go-nat",
"Rev": "50e7633d5f27d81490026a13e5b92d2e42d8c6bb"
},
{
"ImportPath": "github.com/gorilla/context",
"Rev": "14f550f51af52180c2eefed15e5fd18d63c0a64a"
@ -117,10 +121,18 @@
"Comment": "v0.9.0-11-g6b1ef89",
"Rev": "6b1ef893dc11e0447abda6da20a5203481878dda"
},
{
"ImportPath": "github.com/huin/goupnp",
"Rev": "223008361153d7d434c1f0ac990cd3fcae6931f5"
},
{
"ImportPath": "github.com/inconshreveable/go-update",
"Rev": "221d034a558b4c21b0624b2a450c076913854a57"
},
{
"ImportPath": "github.com/jackpal/go-nat-pmp",
"Rev": "a45aa3d54aef73b504e15eb71bea0e5565b5e6e1"
},
{
"ImportPath": "github.com/jbenet/go-base58",
"Rev": "568a28d73fd97651d3442392036a658b6976eed5"
@ -185,7 +197,7 @@
},
{
"ImportPath": "github.com/jbenet/goprocess",
"Rev": "c5455a611a9e0cebac87e7ae421c3273c3224000"
"Rev": "060ad098994e6adac22f0bd889eb756d4f5ad35b"
},
{
"ImportPath": "github.com/kr/binarydist",

191
Godeps/_workspace/src/github.com/fd/go-nat/LICENSE generated vendored Normal file
View File

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

3
Godeps/_workspace/src/github.com/fd/go-nat/README.md generated vendored Normal file
View File

@ -0,0 +1,3 @@
# go-nat
[![GoDoc](https://godoc.org/github.com/fd/go-nat?status.svg)](https://godoc.org/github.com/fd/go-nat) [![status](https://sourcegraph.com/api/repos/github.com/fd/go-nat/.badges/status.png)](https://sourcegraph.com/github.com/fd/go-nat)

54
Godeps/_workspace/src/github.com/fd/go-nat/nat.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
// Package nat implements NAT handling facilities
package nat
import (
"errors"
"math"
"math/rand"
"net"
"time"
)
var ErrNoExternalAddress = errors.New("no external address")
var ErrNoInternalAddress = errors.New("no internal address")
var ErrNoNATFound = errors.New("no NAT found")
// protocol is either "udp" or "tcp"
type NAT interface {
// Type returns the kind of NAT port mapping service that is used
Type() string
// GetDeviceAddress returns the internal address of the gateway device.
GetDeviceAddress() (addr net.IP, err error)
// GetExternalAddress returns the external address of the gateway device.
GetExternalAddress() (addr net.IP, err error)
// GetInternalAddress returns the address of the local host.
GetInternalAddress() (addr net.IP, err error)
// AddPortMapping maps a port on the local host to an external port.
AddPortMapping(protocol string, internalPort int, description string, timeout time.Duration) (mappedExternalPort int, err error)
// DeletePortMapping removes a port mapping.
DeletePortMapping(protocol string, internalPort int) (err error)
}
// DiscoverGateway attempts to find a gateway device.
func DiscoverGateway() (NAT, error) {
select {
case nat := <-discoverUPNP_IG1():
return nat, nil
case nat := <-discoverUPNP_IG2():
return nat, nil
case nat := <-discoverNATPMP():
return nat, nil
case <-time.After(10 * time.Second):
return nil, ErrNoNATFound
}
}
func randomPort() int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(math.MaxUint16-10000) + 10000
}

178
Godeps/_workspace/src/github.com/fd/go-nat/natpmp.go generated vendored Normal file
View File

@ -0,0 +1,178 @@
package nat
import (
"net"
"time"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jackpal/go-nat-pmp"
)
var (
_ NAT = (*natpmp_NAT)(nil)
)
func natpmp_PotentialGateways() ([]net.IP, error) {
_, ipNet_10, err := net.ParseCIDR("10.0.0.0/8")
if err != nil {
panic(err)
}
_, ipNet_172_16, err := net.ParseCIDR("172.16.0.0/12")
if err != nil {
panic(err)
}
_, ipNet_192_168, err := net.ParseCIDR("192.168.0.0/16")
if err != nil {
panic(err)
}
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
var ips []net.IP
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
switch x := addr.(type) {
case *net.IPNet:
var ipNet *net.IPNet
if ipNet_10.Contains(x.IP) {
ipNet = ipNet_10
} else if ipNet_172_16.Contains(x.IP) {
ipNet = ipNet_172_16
} else if ipNet_192_168.Contains(x.IP) {
ipNet = ipNet_192_168
}
if ipNet != nil {
ip := x.IP.Mask(x.Mask)
ip = ip.To4()
if ip != nil {
ip[3] = ip[3] | 0x01
ips = append(ips, ip)
}
}
}
}
}
if len(ips) == 0 {
return nil, ErrNoNATFound
}
return ips, nil
}
func discoverNATPMP() <-chan NAT {
ips, err := natpmp_PotentialGateways()
if err != nil {
return nil
}
res := make(chan NAT, len(ips))
for _, ip := range ips {
go discoverNATPMPWithAddr(res, ip)
}
return res
}
func discoverNATPMPWithAddr(c chan NAT, ip net.IP) {
client := natpmp.NewClient(ip)
_, err := client.GetExternalAddress()
if err != nil {
return
}
c <- &natpmp_NAT{client, ip, make(map[int]int)}
}
type natpmp_NAT struct {
c *natpmp.Client
gateway net.IP
ports map[int]int
}
func (n *natpmp_NAT) GetDeviceAddress() (addr net.IP, err error) {
return n.gateway, nil
}
func (n *natpmp_NAT) GetInternalAddress() (addr net.IP, err error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
switch x := addr.(type) {
case *net.IPNet:
if x.Contains(n.gateway) {
return x.IP, nil
}
}
}
}
return nil, ErrNoInternalAddress
}
func (n *natpmp_NAT) GetExternalAddress() (addr net.IP, err error) {
res, err := n.c.GetExternalAddress()
if err != nil {
return nil, err
}
d := res.ExternalIPAddress
return net.IPv4(d[0], d[1], d[2], d[3]), nil
}
func (n *natpmp_NAT) AddPortMapping(protocol string, internalPort int, description string, timeout time.Duration) (int, error) {
var (
err error
)
timeoutInSeconds := int(timeout / time.Second)
if externalPort := n.ports[internalPort]; externalPort > 0 {
_, err = n.c.AddPortMapping(protocol, internalPort, externalPort, timeoutInSeconds)
if err == nil {
n.ports[internalPort] = externalPort
return externalPort, nil
}
}
for i := 0; i < 3; i++ {
externalPort := randomPort()
_, err = n.c.AddPortMapping(protocol, internalPort, externalPort, timeoutInSeconds)
if err == nil {
n.ports[internalPort] = externalPort
return externalPort, nil
}
}
return 0, err
}
func (n *natpmp_NAT) DeletePortMapping(protocol string, internalPort int) (err error) {
delete(n.ports, internalPort)
return nil
}
func (n *natpmp_NAT) Type() string {
return "NAT-PMP"
}

239
Godeps/_workspace/src/github.com/fd/go-nat/upnp.go generated vendored Normal file
View File

@ -0,0 +1,239 @@
package nat
import (
"net"
"time"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/dcps/internetgateway1"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/dcps/internetgateway2"
)
var (
_ NAT = (*upnp_NAT)(nil)
)
func discoverUPNP_IG1() <-chan NAT {
res := make(chan NAT, 1)
go func() {
// find devices
devs, err := goupnp.DiscoverDevices(internetgateway1.URN_WANConnectionDevice_1)
if err != nil {
return
}
for _, dev := range devs {
if dev.Root == nil {
continue
}
dev.Root.Device.VisitServices(func(srv *goupnp.Service) {
switch srv.ServiceType {
case internetgateway1.URN_WANIPConnection_1:
client := &internetgateway1.WANIPConnection1{ServiceClient: goupnp.ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: dev.Root,
Service: srv,
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-IP1)", dev.Root}
return
}
case internetgateway1.URN_WANPPPConnection_1:
client := &internetgateway1.WANPPPConnection1{ServiceClient: goupnp.ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: dev.Root,
Service: srv,
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG1-PPP1)", dev.Root}
return
}
}
})
}
}()
return res
}
func discoverUPNP_IG2() <-chan NAT {
res := make(chan NAT, 1)
go func() {
// find devices
devs, err := goupnp.DiscoverDevices(internetgateway2.URN_WANConnectionDevice_2)
if err != nil {
return
}
for _, dev := range devs {
if dev.Root == nil {
continue
}
dev.Root.Device.VisitServices(func(srv *goupnp.Service) {
switch srv.ServiceType {
case internetgateway2.URN_WANIPConnection_1:
client := &internetgateway2.WANIPConnection1{ServiceClient: goupnp.ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: dev.Root,
Service: srv,
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-IP1)", dev.Root}
return
}
case internetgateway2.URN_WANIPConnection_2:
client := &internetgateway2.WANIPConnection2{ServiceClient: goupnp.ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: dev.Root,
Service: srv,
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-IP2)", dev.Root}
return
}
case internetgateway2.URN_WANPPPConnection_1:
client := &internetgateway2.WANPPPConnection1{ServiceClient: goupnp.ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: dev.Root,
Service: srv,
}}
_, isNat, err := client.GetNATRSIPStatus()
if err == nil && isNat {
res <- &upnp_NAT{client, make(map[int]int), "UPNP (IG2-PPP1)", dev.Root}
return
}
}
})
}
}()
return res
}
type upnp_NAT_Client interface {
GetExternalIPAddress() (string, error)
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
DeletePortMapping(string, uint16, string) error
}
type upnp_NAT struct {
c upnp_NAT_Client
ports map[int]int
typ string
rootDevice *goupnp.RootDevice
}
func (u *upnp_NAT) GetExternalAddress() (addr net.IP, err error) {
ipString, err := u.c.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, ErrNoExternalAddress
}
return ip, nil
}
func mapProtocol(s string) string {
switch s {
case "udp":
return "UDP"
case "tcp":
return "TCP"
default:
panic("invalid protocol: " + s)
}
}
func (u *upnp_NAT) AddPortMapping(protocol string, internalPort int, description string, timeout time.Duration) (int, error) {
ip, err := u.GetInternalAddress()
if err != nil {
return 0, nil
}
timeoutInSeconds := uint32(timeout / time.Second)
if externalPort := u.ports[internalPort]; externalPort > 0 {
err = u.c.AddPortMapping("", uint16(externalPort), mapProtocol(protocol), uint16(internalPort), ip.String(), true, description, timeoutInSeconds)
if err == nil {
return externalPort, nil
}
}
for i := 0; i < 3; i++ {
externalPort := randomPort()
err = u.c.AddPortMapping("", uint16(externalPort), mapProtocol(protocol), uint16(internalPort), ip.String(), true, description, timeoutInSeconds)
if err == nil {
u.ports[internalPort] = externalPort
return externalPort, nil
}
}
return 0, err
}
func (u *upnp_NAT) DeletePortMapping(protocol string, internalPort int) error {
if externalPort := u.ports[internalPort]; externalPort > 0 {
delete(u.ports, internalPort)
return u.c.DeletePortMapping("", uint16(externalPort), mapProtocol(protocol))
}
return nil
}
func (u *upnp_NAT) GetDeviceAddress() (net.IP, error) {
addr, err := net.ResolveUDPAddr("udp4", u.rootDevice.URLBase.Host)
if err != nil {
return nil, err
}
return addr.IP, nil
}
func (u *upnp_NAT) GetInternalAddress() (net.IP, error) {
devAddr, err := u.GetDeviceAddress()
if err != nil {
return nil, err
}
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
switch x := addr.(type) {
case *net.IPNet:
if x.Contains(devAddr) {
return x.IP, nil
}
}
}
}
return nil, ErrNoInternalAddress
}
func (n *upnp_NAT) Type() string { return n.typ }

23
Godeps/_workspace/src/github.com/huin/goupnp/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright (c) 2013, John Beisley <greatred@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

14
Godeps/_workspace/src/github.com/huin/goupnp/README.md generated vendored Normal file
View File

@ -0,0 +1,14 @@
goupnp is a UPnP client library for Go
Installation
------------
Run `go get -u github.com/huin/goupnp`.
Regenerating dcps generated source code:
----------------------------------------
1. Install gotasks: `go get -u github.com/jingweno/gotask`
2. Change to the gotasks directory: `cd gotasks`
3. Download UPnP specification data (if not done already): `wget http://upnp.org/resources/upnpresources.zip`
4. Regenerate source code: `gotask specgen -s upnpresources.zip -o ../dcps`

View File

@ -0,0 +1,20 @@
package main
import (
"log"
"net/http"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/httpu"
)
func main() {
srv := httpu.Server{
Addr: "239.255.255.250:1900",
Multicast: true,
Handler: httpu.HandlerFunc(func(r *http.Request) {
log.Printf("Got %s %s message from %v: %v", r.Method, r.URL.Path, r.RemoteAddr, r.Header)
}),
}
err := srv.ListenAndServe()
log.Printf("Serving failed with error: %v", err)
}

View File

@ -0,0 +1,67 @@
package main
import (
"fmt"
"log"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/dcps/internetgateway1"
)
func main() {
clients, errors, err := internetgateway1.NewWANPPPConnection1Clients()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Got %d errors finding servers and %d successfully discovered.\n",
len(errors), len(clients))
for i, e := range errors {
fmt.Printf("Error finding server #%d: %v\n", i+1, e)
}
for _, c := range clients {
dev := &c.ServiceClient.RootDevice.Device
srv := c.ServiceClient.Service
fmt.Println(dev.FriendlyName, " :: ", srv.String())
scpd, err := srv.RequestSCDP()
if err != nil {
fmt.Printf(" Error requesting service SCPD: %v\n", err)
} else {
fmt.Println(" Available actions:")
for _, action := range scpd.Actions {
fmt.Printf(" * %s\n", action.Name)
for _, arg := range action.Arguments {
var varDesc string
if stateVar := scpd.GetStateVariable(arg.RelatedStateVariable); stateVar != nil {
varDesc = fmt.Sprintf(" (%s)", stateVar.DataType.Name)
}
fmt.Printf(" * [%s] %s%s\n", arg.Direction, arg.Name, varDesc)
}
}
}
if scpd == nil || scpd.GetAction("GetExternalIPAddress") != nil {
ip, err := c.GetExternalIPAddress()
fmt.Println("GetExternalIPAddress: ", ip, err)
}
if scpd == nil || scpd.GetAction("GetStatusInfo") != nil {
status, lastErr, uptime, err := c.GetStatusInfo()
fmt.Println("GetStatusInfo: ", status, lastErr, uptime, err)
}
if scpd == nil || scpd.GetAction("GetIdleDisconnectTime") != nil {
idleTime, err := c.GetIdleDisconnectTime()
fmt.Println("GetIdleDisconnectTime: ", idleTime, err)
}
if scpd == nil || scpd.GetAction("AddPortMapping") != nil {
err := c.AddPortMapping("", 5000, "TCP", 5001, "192.168.1.2", true, "Test port mapping", 0)
fmt.Println("AddPortMapping: ", err)
}
if scpd == nil || scpd.GetAction("DeletePortMapping") != nil {
err := c.DeletePortMapping("", 5000, "TCP")
fmt.Println("DeletePortMapping: ", err)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

184
Godeps/_workspace/src/github.com/huin/goupnp/device.go generated vendored Normal file
View File

@ -0,0 +1,184 @@
// This file contains XML structures for communicating with UPnP devices.
package goupnp
import (
"encoding/xml"
"errors"
"fmt"
"net/url"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/scpd"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/soap"
)
const (
DeviceXMLNamespace = "urn:schemas-upnp-org:device-1-0"
)
// RootDevice is the device description as described by section 2.3 "Device
// description" in
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
type RootDevice struct {
XMLName xml.Name `xml:"root"`
SpecVersion SpecVersion `xml:"specVersion"`
URLBase url.URL `xml:"-"`
URLBaseStr string `xml:"URLBase"`
Device Device `xml:"device"`
}
// SetURLBase sets the URLBase for the RootDevice and its underlying components.
func (root *RootDevice) SetURLBase(urlBase *url.URL) {
root.URLBase = *urlBase
root.URLBaseStr = urlBase.String()
root.Device.SetURLBase(urlBase)
}
// SpecVersion is part of a RootDevice, describes the version of the
// specification that the data adheres to.
type SpecVersion struct {
Major int32 `xml:"major"`
Minor int32 `xml:"minor"`
}
// Device is a UPnP device. It can have child devices.
type Device struct {
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"`
ManufacturerURL URLField `xml:"manufacturerURL"`
ModelDescription string `xml:"modelDescription"`
ModelName string `xml:"modelName"`
ModelNumber string `xml:"modelNumber"`
ModelURL URLField `xml:"modelURL"`
SerialNumber string `xml:"serialNumber"`
UDN string `xml:"UDN"`
UPC string `xml:"UPC,omitempty"`
Icons []Icon `xml:"iconList>icon,omitempty"`
Services []Service `xml:"serviceList>service,omitempty"`
Devices []Device `xml:"deviceList>device,omitempty"`
// Extra observed elements:
PresentationURL URLField `xml:"presentationURL"`
}
// VisitDevices calls visitor for the device, and all its descendent devices.
func (device *Device) VisitDevices(visitor func(*Device)) {
visitor(device)
for i := range device.Devices {
device.Devices[i].VisitDevices(visitor)
}
}
// VisitServices calls visitor for all Services under the device and all its
// descendent devices.
func (device *Device) VisitServices(visitor func(*Service)) {
device.VisitDevices(func(d *Device) {
for i := range d.Services {
visitor(&d.Services[i])
}
})
}
// FindService finds all (if any) Services under the device and its descendents
// that have the given ServiceType.
func (device *Device) FindService(serviceType string) []*Service {
var services []*Service
device.VisitServices(func(s *Service) {
if s.ServiceType == serviceType {
services = append(services, s)
}
})
return services
}
// SetURLBase sets the URLBase for the Device and its underlying components.
func (device *Device) SetURLBase(urlBase *url.URL) {
device.ManufacturerURL.SetURLBase(urlBase)
device.ModelURL.SetURLBase(urlBase)
device.PresentationURL.SetURLBase(urlBase)
for i := range device.Icons {
device.Icons[i].SetURLBase(urlBase)
}
for i := range device.Services {
device.Services[i].SetURLBase(urlBase)
}
for i := range device.Devices {
device.Devices[i].SetURLBase(urlBase)
}
}
func (device *Device) String() string {
return fmt.Sprintf("Device ID %s : %s (%s)", device.UDN, device.DeviceType, device.FriendlyName)
}
// Icon is a representative image that a device might include in its
// description.
type Icon struct {
Mimetype string `xml:"mimetype"`
Width int32 `xml:"width"`
Height int32 `xml:"height"`
Depth int32 `xml:"depth"`
URL URLField `xml:"url"`
}
// SetURLBase sets the URLBase for the Icon.
func (icon *Icon) SetURLBase(url *url.URL) {
icon.URL.SetURLBase(url)
}
// Service is a service provided by a UPnP Device.
type Service struct {
ServiceType string `xml:"serviceType"`
ServiceId string `xml:"serviceId"`
SCPDURL URLField `xml:"SCPDURL"`
ControlURL URLField `xml:"controlURL"`
EventSubURL URLField `xml:"eventSubURL"`
}
// SetURLBase sets the URLBase for the Service.
func (srv *Service) SetURLBase(urlBase *url.URL) {
srv.SCPDURL.SetURLBase(urlBase)
srv.ControlURL.SetURLBase(urlBase)
srv.EventSubURL.SetURLBase(urlBase)
}
func (srv *Service) String() string {
return fmt.Sprintf("Service ID %s : %s", srv.ServiceId, srv.ServiceType)
}
// RequestSCDP requests the SCPD (soap actions and state variables description)
// for the service.
func (srv *Service) RequestSCDP() (*scpd.SCPD, error) {
if !srv.SCPDURL.Ok {
return nil, errors.New("bad/missing SCPD URL, or no URLBase has been set")
}
s := new(scpd.SCPD)
if err := requestXml(srv.SCPDURL.URL.String(), scpd.SCPDXMLNamespace, s); err != nil {
return nil, err
}
return s, nil
}
func (srv *Service) NewSOAPClient() *soap.SOAPClient {
return soap.NewSOAPClient(srv.ControlURL.URL)
}
// URLField is a URL that is part of a device description.
type URLField struct {
URL url.URL `xml:"-"`
Ok bool `xml:"-"`
Str string `xml:",chardata"`
}
func (uf *URLField) SetURLBase(urlBase *url.URL) {
refUrl, err := url.Parse(uf.Str)
if err != nil {
uf.URL = url.URL{}
uf.Ok = false
return
}
uf.URL = *urlBase.ResolveReference(refUrl)
uf.Ok = true
}

View File

@ -0,0 +1,6 @@
// Serves as examples of using the goupnp library.
//
// To run examples and see the output for your local network, run the following
// command (specifically including the -v flag):
// go test -v github.com/huin/goupnp/example
package example

View File

@ -0,0 +1,62 @@
package example_test
import (
"fmt"
"os"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/dcps/internetgateway1"
)
// Use discovered WANPPPConnection1 services to find external IP addresses.
func Example_WANPPPConnection1_GetExternalIPAddress() {
clients, errors, err := internetgateway1.NewWANPPPConnection1Clients()
extIPClients := make([]GetExternalIPAddresser, len(clients))
for i, client := range clients {
extIPClients[i] = client
}
DisplayExternalIPResults(extIPClients, errors, err)
// Output:
}
// Use discovered WANIPConnection services to find external IP addresses.
func Example_WANIPConnection_GetExternalIPAddress() {
clients, errors, err := internetgateway1.NewWANIPConnection1Clients()
extIPClients := make([]GetExternalIPAddresser, len(clients))
for i, client := range clients {
extIPClients[i] = client
}
DisplayExternalIPResults(extIPClients, errors, err)
// Output:
}
type GetExternalIPAddresser interface {
GetExternalIPAddress() (NewExternalIPAddress string, err error)
GetServiceClient() *goupnp.ServiceClient
}
func DisplayExternalIPResults(clients []GetExternalIPAddresser, errors []error, err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "Error discovering service with UPnP: ", err)
return
}
if len(errors) > 0 {
fmt.Fprintf(os.Stderr, "Error discovering %d services:\n", len(errors))
for _, err := range errors {
fmt.Println(" ", err)
}
}
fmt.Fprintf(os.Stderr, "Successfully discovered %d services:\n", len(clients))
for _, client := range clients {
device := &client.GetServiceClient().RootDevice.Device
fmt.Fprintln(os.Stderr, " Device:", device.FriendlyName)
if addr, err := client.GetExternalIPAddress(); err != nil {
fmt.Fprintf(os.Stderr, " Failed to get external IP address: %v\n", err)
} else {
fmt.Fprintf(os.Stderr, " External IP address: %v\n", addr)
}
}
}

View File

@ -0,0 +1,539 @@
// +build gotask
package gotasks
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"text/template"
"github.com/huin/goutil/codegen"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/scpd"
"github.com/jingweno/gotask/tasking"
)
var (
deviceURNPrefix = "urn:schemas-upnp-org:device:"
serviceURNPrefix = "urn:schemas-upnp-org:service:"
)
// NAME
// specgen - generates Go code from the UPnP specification files.
//
// DESCRIPTION
// The specification is available for download from:
//
// OPTIONS
// -s, --spec_filename=<upnpresources.zip>
// Path to the specification file, available from http://upnp.org/resources/upnpresources.zip
// -o, --out_dir=<output directory>
// Path to the output directory. This is is where the DCP source files will be placed. Should normally correspond to the directory for github.com/huin/goupnp/dcps
// --nogofmt
// Disable passing the output through gofmt. Do this if debugging code output problems and needing to see the generated code prior to being passed through gofmt.
func TaskSpecgen(t *tasking.T) {
specFilename := t.Flags.String("spec-filename")
if specFilename == "" {
specFilename = t.Flags.String("s")
}
if specFilename == "" {
t.Fatal("--spec_filename is required")
}
outDir := t.Flags.String("out-dir")
if outDir == "" {
outDir = t.Flags.String("o")
}
if outDir == "" {
log.Fatal("--out_dir is required")
}
useGofmt := !t.Flags.Bool("nogofmt")
specArchive, err := openZipfile(specFilename)
if err != nil {
t.Fatalf("Error opening spec file: %v", err)
}
defer specArchive.Close()
dcpCol := newDcpsCollection()
for _, f := range globFiles("standardizeddcps/*/*.zip", specArchive.Reader) {
dirName := strings.TrimPrefix(f.Name, "standardizeddcps/")
slashIndex := strings.Index(dirName, "/")
if slashIndex == -1 {
// Should not happen.
t.Logf("Could not find / in %q", dirName)
return
}
dirName = dirName[:slashIndex]
dcp := dcpCol.dcpForDir(dirName)
if dcp == nil {
t.Logf("No alias defined for directory %q: skipping %s\n", dirName, f.Name)
continue
} else {
t.Logf("Alias found for directory %q: processing %s\n", dirName, f.Name)
}
dcp.processZipFile(f)
}
for _, dcp := range dcpCol.dcpByAlias {
if err := dcp.writePackage(outDir, useGofmt); err != nil {
log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err)
}
}
}
// DCP contains extra metadata to use when generating DCP source files.
type DCPMetadata struct {
Name string // What to name the Go DCP package.
OfficialName string // Official name for the DCP.
DocURL string // Optional - URL for futher documentation about the DCP.
}
var dcpMetadataByDir = map[string]DCPMetadata{
"Internet Gateway_1": {
Name: "internetgateway1",
OfficialName: "Internet Gateway Device v1",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
},
"Internet Gateway_2": {
Name: "internetgateway2",
OfficialName: "Internet Gateway Device v2",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
},
}
type dcpCollection struct {
dcpByAlias map[string]*DCP
}
func newDcpsCollection() *dcpCollection {
c := &dcpCollection{
dcpByAlias: make(map[string]*DCP),
}
for _, metadata := range dcpMetadataByDir {
c.dcpByAlias[metadata.Name] = newDCP(metadata)
}
return c
}
func (c *dcpCollection) dcpForDir(dirName string) *DCP {
metadata, ok := dcpMetadataByDir[dirName]
if !ok {
return nil
}
return c.dcpByAlias[metadata.Name]
}
// DCP collects together information about a UPnP Device Control Protocol.
type DCP struct {
Metadata DCPMetadata
DeviceTypes map[string]*URNParts
ServiceTypes map[string]*URNParts
Services []SCPDWithURN
}
func newDCP(metadata DCPMetadata) *DCP {
return &DCP{
Metadata: metadata,
DeviceTypes: make(map[string]*URNParts),
ServiceTypes: make(map[string]*URNParts),
}
}
func (dcp *DCP) processZipFile(file *zip.File) {
archive, err := openChildZip(file)
if err != nil {
log.Println("Error reading child zip file:", err)
return
}
for _, deviceFile := range globFiles("*/device/*.xml", archive) {
dcp.processDeviceFile(deviceFile)
}
for _, scpdFile := range globFiles("*/service/*.xml", archive) {
dcp.processSCPDFile(scpdFile)
}
}
func (dcp *DCP) processDeviceFile(file *zip.File) {
var device goupnp.Device
if err := unmarshalXmlFile(file, &device); err != nil {
log.Printf("Error decoding device XML from file %q: %v", file.Name, err)
return
}
device.VisitDevices(func(d *goupnp.Device) {
t := strings.TrimSpace(d.DeviceType)
if t != "" {
u, err := extractURNParts(t, deviceURNPrefix)
if err != nil {
log.Println(err)
return
}
dcp.DeviceTypes[t] = u
}
})
device.VisitServices(func(s *goupnp.Service) {
u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
if err != nil {
log.Println(err)
return
}
dcp.ServiceTypes[s.ServiceType] = u
})
}
func (dcp *DCP) writePackage(outDir string, useGofmt bool) error {
packageDirname := filepath.Join(outDir, dcp.Metadata.Name)
err := os.MkdirAll(packageDirname, os.ModePerm)
if err != nil && !os.IsExist(err) {
return err
}
packageFilename := filepath.Join(packageDirname, dcp.Metadata.Name+".go")
packageFile, err := os.Create(packageFilename)
if err != nil {
return err
}
var output io.WriteCloser = packageFile
if useGofmt {
if output, err = codegen.NewGofmtWriteCloser(output); err != nil {
packageFile.Close()
return err
}
}
if err = packageTmpl.Execute(output, dcp); err != nil {
output.Close()
return err
}
return output.Close()
}
func (dcp *DCP) processSCPDFile(file *zip.File) {
scpd := new(scpd.SCPD)
if err := unmarshalXmlFile(file, scpd); err != nil {
log.Printf("Error decoding SCPD XML from file %q: %v", file.Name, err)
return
}
scpd.Clean()
urnParts, err := urnPartsFromSCPDFilename(file.Name)
if err != nil {
log.Printf("Could not recognize SCPD filename %q: %v", file.Name, err)
return
}
dcp.Services = append(dcp.Services, SCPDWithURN{
URNParts: urnParts,
SCPD: scpd,
})
}
type SCPDWithURN struct {
*URNParts
SCPD *scpd.SCPD
}
func (s *SCPDWithURN) WrapArgument(arg scpd.Argument) (*argumentWrapper, error) {
relVar := s.SCPD.GetStateVariable(arg.RelatedStateVariable)
if relVar == nil {
return nil, fmt.Errorf("no such state variable: %q, for argument %q", arg.RelatedStateVariable, arg.Name)
}
cnv, ok := typeConvs[relVar.DataType.Name]
if !ok {
return nil, fmt.Errorf("unknown data type: %q, for state variable %q, for argument %q", relVar.DataType.Type, arg.RelatedStateVariable, arg.Name)
}
return &argumentWrapper{
Argument: arg,
relVar: relVar,
conv: cnv,
}, nil
}
type argumentWrapper struct {
scpd.Argument
relVar *scpd.StateVariable
conv conv
}
func (arg *argumentWrapper) AsParameter() string {
return fmt.Sprintf("%s %s", arg.Name, arg.conv.ExtType)
}
func (arg *argumentWrapper) Document() string {
relVar := arg.relVar
if rng := relVar.AllowedValueRange; rng != nil {
var parts []string
if rng.Minimum != "" {
parts = append(parts, fmt.Sprintf("minimum=%s", rng.Minimum))
}
if rng.Maximum != "" {
parts = append(parts, fmt.Sprintf("maximum=%s", rng.Maximum))
}
if rng.Step != "" {
parts = append(parts, fmt.Sprintf("step=%s", rng.Step))
}
return "allowed value range: " + strings.Join(parts, ", ")
}
if len(relVar.AllowedValues) != 0 {
return "allowed values: " + strings.Join(relVar.AllowedValues, ", ")
}
return ""
}
func (arg *argumentWrapper) Marshal() string {
return fmt.Sprintf("soap.Marshal%s(%s)", arg.conv.FuncSuffix, arg.Name)
}
func (arg *argumentWrapper) Unmarshal(objVar string) string {
return fmt.Sprintf("soap.Unmarshal%s(%s.%s)", arg.conv.FuncSuffix, objVar, arg.Name)
}
type conv struct {
FuncSuffix string
ExtType string
}
// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name
// suffix inside the soap module (e.g "Fixed14_4") and the Go type.
var typeConvs = map[string]conv{
"ui1": conv{"Ui1", "uint8"},
"ui2": conv{"Ui2", "uint16"},
"ui4": conv{"Ui4", "uint32"},
"i1": conv{"I1", "int8"},
"i2": conv{"I2", "int16"},
"i4": conv{"I4", "int32"},
"int": conv{"Int", "int64"},
"r4": conv{"R4", "float32"},
"r8": conv{"R8", "float64"},
"number": conv{"R8", "float64"}, // Alias for r8.
"fixed.14.4": conv{"Fixed14_4", "float64"},
"float": conv{"R8", "float64"},
"char": conv{"Char", "rune"},
"string": conv{"String", "string"},
"date": conv{"Date", "time.Time"},
"dateTime": conv{"DateTime", "time.Time"},
"dateTime.tz": conv{"DateTimeTz", "time.Time"},
"time": conv{"TimeOfDay", "soap.TimeOfDay"},
"time.tz": conv{"TimeOfDayTz", "soap.TimeOfDay"},
"boolean": conv{"Boolean", "bool"},
"bin.base64": conv{"BinBase64", "[]byte"},
"bin.hex": conv{"BinHex", "[]byte"},
}
type closeableZipReader struct {
io.Closer
*zip.Reader
}
func openZipfile(filename string) (*closeableZipReader, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
fi, err := file.Stat()
if err != nil {
return nil, err
}
archive, err := zip.NewReader(file, fi.Size())
if err != nil {
return nil, err
}
return &closeableZipReader{
Closer: file,
Reader: archive,
}, nil
}
// openChildZip opens a zip file within another zip file.
func openChildZip(file *zip.File) (*zip.Reader, error) {
zipFile, err := file.Open()
if err != nil {
return nil, err
}
defer zipFile.Close()
zipBytes, err := ioutil.ReadAll(zipFile)
if err != nil {
return nil, err
}
return zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
}
func globFiles(pattern string, archive *zip.Reader) []*zip.File {
var files []*zip.File
for _, f := range archive.File {
if matched, err := path.Match(pattern, f.Name); err != nil {
// This shouldn't happen - all patterns are hard-coded, errors in them
// are a programming error.
panic(err)
} else if matched {
files = append(files, f)
}
}
return files
}
func unmarshalXmlFile(file *zip.File, data interface{}) error {
r, err := file.Open()
if err != nil {
return err
}
decoder := xml.NewDecoder(r)
r.Close()
return decoder.Decode(data)
}
type URNParts struct {
URN string
Name string
Version string
}
func (u *URNParts) Const() string {
return fmt.Sprintf("URN_%s_%s", u.Name, u.Version)
}
// extractURNParts extracts the name and version from a URN string.
func extractURNParts(urn, expectedPrefix string) (*URNParts, error) {
if !strings.HasPrefix(urn, expectedPrefix) {
return nil, fmt.Errorf("%q does not have expected prefix %q", urn, expectedPrefix)
}
parts := strings.SplitN(strings.TrimPrefix(urn, expectedPrefix), ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("%q does not have a name and version", urn)
}
name, version := parts[0], parts[1]
return &URNParts{urn, name, version}, nil
}
var scpdFilenameRe = regexp.MustCompile(
`.*/([a-zA-Z0-9]+)([0-9]+)\.xml`)
func urnPartsFromSCPDFilename(filename string) (*URNParts, error) {
parts := scpdFilenameRe.FindStringSubmatch(filename)
if len(parts) != 3 {
return nil, fmt.Errorf("SCPD filename %q does not have expected number of parts", filename)
}
name, version := parts[1], parts[2]
return &URNParts{
URN: serviceURNPrefix + name + ":" + version,
Name: name,
Version: version,
}, nil
}
var packageTmpl = template.Must(template.New("package").Parse(`{{$name := .Metadata.Name}}
// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}.
// {{if .Metadata.DocURL}}
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
//
// Typically, use one of the New* functions to discover services on the local
// network.
package {{$name}}
// Generated file - do not edit by hand. See README.md
import (
"time"
"github.com/huin/goupnp"
"github.com/huin/goupnp/soap"
)
// Hack to avoid Go complaining if time isn't used.
var _ time.Time
// Device URNs:
const ({{range .DeviceTypes}}
{{.Const}} = "{{.URN}}"{{end}}
)
// Service URNs:
const ({{range .ServiceTypes}}
{{.Const}} = "{{.URN}}"{{end}}
)
{{range .Services}}
{{$srv := .}}
{{$srvIdent := printf "%s%s" .Name .Version}}
// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See
// goupnp.ServiceClient, which contains RootDevice and Service attributes which
// are provided for informational value.
type {{$srvIdent}} struct {
goupnp.ServiceClient
}
// New{{$srvIdent}}Clients discovers instances of the service on the network,
// and returns clients to any that are found. errors will contain an error for
// any devices that replied but which could not be queried, and err will be set
// if the discovery process failed outright.
//
// This is a typical entry calling point into this package.
func New{{$srvIdent}}Clients() (clients []*{{$srvIdent}}, errors []error, err error) {
var genericClients []goupnp.ServiceClient
if genericClients, errors, err = goupnp.NewServiceClients({{$srv.Const}}); err != nil {
return
}
clients = make([]*{{$srvIdent}}, len(genericClients))
for i := range genericClients {
clients[i] = &{{$srvIdent}}{genericClients[i]}
}
return
}
{{range .SCPD.Actions}}{{/* loops over *SCPDWithURN values */}}
{{$inargs := .InputArguments}}{{$outargs := .OutputArguments}}
// {{if $inargs}}Arguments:{{range $inargs}}{{$argWrap := $srv.WrapArgument .}}
//
// * {{.Name}}: {{$argWrap.Document}}{{end}}{{end}}
//
// {{if $outargs}}Return values:{{range $outargs}}{{$argWrap := $srv.WrapArgument .}}
//
// * {{.Name}}: {{$argWrap.Document}}{{end}}{{end}}
func (client *{{$srvIdent}}) {{.Name}}({{range $inargs}}{{/*
*/}}{{$argWrap := $srv.WrapArgument .}}{{$argWrap.AsParameter}}, {{end}}{{/*
*/}}) ({{range $outargs}}{{/*
*/}}{{$argWrap := $srv.WrapArgument .}}{{$argWrap.AsParameter}}, {{end}} err error) {
// Request structure.
request := {{if $inargs}}&{{template "argstruct" $inargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
// BEGIN Marshal arguments into request.
{{range $inargs}}{{$argWrap := $srv.WrapArgument .}}
if request.{{.Name}}, err = {{$argWrap.Marshal}}; err != nil {
return
}{{end}}
// END Marshal arguments into request.
// Response structure.
response := {{if $outargs}}&{{template "argstruct" $outargs}}{{"{}"}}{{else}}{{"interface{}(nil)"}}{{end}}
// Perform the SOAP call.
if err = client.SOAPClient.PerformAction({{$srv.URNParts.Const}}, "{{.Name}}", request, response); err != nil {
return
}
// BEGIN Unmarshal arguments from response.
{{range $outargs}}{{$argWrap := $srv.WrapArgument .}}
if {{.Name}}, err = {{$argWrap.Unmarshal "response"}}; err != nil {
return
}{{end}}
// END Unmarshal arguments from response.
return
}
{{end}}{{/* range .SCPD.Actions */}}
{{end}}{{/* range .Services */}}
{{define "argstruct"}}struct {{"{"}}{{range .}}
{{.Name}} string
{{end}}{{"}"}}{{end}}
`))

109
Godeps/_workspace/src/github.com/huin/goupnp/goupnp.go generated vendored Normal file
View File

@ -0,0 +1,109 @@
// goupnp is an implementation of a client for various UPnP services.
//
// For most uses, it is recommended to use the code-generated packages under
// github.com/huin/goupnp/dcps. Example use is shown at
// http://godoc.org/github.com/huin/goupnp/example
//
// A commonly used client is internetgateway1.WANPPPConnection1:
// http://godoc.org/github.com/huin/goupnp/dcps/internetgateway1#WANPPPConnection1
//
// Currently only a couple of schemas have code generated for them from the
// UPnP example XML specifications. Not all methods will work on these clients,
// because the generated stubs contain the full set of specified methods from
// the XML specifications, and the discovered services will likely support a
// subset of those methods.
package goupnp
import (
"encoding/xml"
"fmt"
"net/http"
"net/url"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/httpu"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/ssdp"
)
// ContextError is an error that wraps an error with some context information.
type ContextError struct {
Context string
Err error
}
func (err ContextError) Error() string {
return fmt.Sprintf("%s: %v", err.Context, err.Err)
}
// MaybeRootDevice contains either a RootDevice or an error.
type MaybeRootDevice struct {
Root *RootDevice
Err error
}
// DiscoverDevices attempts to find targets of the given type. This is
// typically the entry-point for this package. searchTarget is typically a URN
// in the form "urn:schemas-upnp-org:device:..." or
// "urn:schemas-upnp-org:service:...". A single error is returned for errors
// while attempting to send the query. An error or RootDevice is returned for
// each discovered RootDevice.
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
httpu, err := httpu.NewHTTPUClient()
if err != nil {
return nil, err
}
defer httpu.Close()
responses, err := ssdp.SSDPRawSearch(httpu, string(searchTarget), 2, 3)
if err != nil {
return nil, err
}
results := make([]MaybeRootDevice, len(responses))
for i, response := range responses {
maybe := &results[i]
loc, err := response.Location()
if err != nil {
maybe.Err = ContextError{"unexpected bad location from search", err}
continue
}
locStr := loc.String()
root := new(RootDevice)
if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil {
maybe.Err = ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
continue
}
var urlBaseStr string
if root.URLBaseStr != "" {
urlBaseStr = root.URLBaseStr
} else {
urlBaseStr = locStr
}
urlBase, err := url.Parse(urlBaseStr)
if err != nil {
maybe.Err = ContextError{fmt.Sprintf("error parsing location URL %q", locStr), err}
continue
}
root.SetURLBase(urlBase)
maybe.Root = root
}
return results, nil
}
func requestXml(url string, defaultSpace string, doc interface{}) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("goupnp: got response status %s from %q",
resp.Status, url)
}
decoder := xml.NewDecoder(resp.Body)
decoder.DefaultSpace = defaultSpace
return decoder.Decode(doc)
}

View File

@ -0,0 +1,117 @@
package httpu
import (
"bufio"
"bytes"
"fmt"
"log"
"net"
"net/http"
"sync"
"time"
)
// HTTPUClient is a client for dealing with HTTPU (HTTP over UDP). Its typical
// function is for HTTPMU, and particularly SSDP.
type HTTPUClient struct {
connLock sync.Mutex // Protects use of conn.
conn net.PacketConn
}
// NewHTTPUClient creates a new HTTPUClient, opening up a new UDP socket for the
// purpose.
func NewHTTPUClient() (*HTTPUClient, error) {
conn, err := net.ListenPacket("udp", ":0")
if err != nil {
return nil, err
}
return &HTTPUClient{conn: conn}, nil
}
// Close shuts down the client. The client will no longer be useful following
// this.
func (httpu *HTTPUClient) Close() error {
httpu.connLock.Lock()
defer httpu.connLock.Unlock()
return httpu.conn.Close()
}
// Do performs a request. The timeout is how long to wait for before returning
// the responses that were received. An error is only returned for failing to
// send the request. Failures in receipt simply do not add to the resulting
// responses.
//
// Note that at present only one concurrent connection will happen per
// HTTPUClient.
func (httpu *HTTPUClient) Do(req *http.Request, timeout time.Duration, numSends int) ([]*http.Response, error) {
httpu.connLock.Lock()
defer httpu.connLock.Unlock()
// Create the request. This is a subset of what http.Request.Write does
// deliberately to avoid creating extra fields which may confuse some
// devices.
var requestBuf bytes.Buffer
method := req.Method
if method == "" {
method = "GET"
}
if _, err := fmt.Fprintf(&requestBuf, "%s %s HTTP/1.1\r\n", method, req.URL.RequestURI()); err != nil {
return nil, err
}
if err := req.Header.Write(&requestBuf); err != nil {
return nil, err
}
if _, err := requestBuf.Write([]byte{'\r', '\n'}); err != nil {
return nil, err
}
destAddr, err := net.ResolveUDPAddr("udp", req.Host)
if err != nil {
return nil, err
}
if err = httpu.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
return nil, err
}
// Send request.
for i := 0; i < numSends; i++ {
if n, err := httpu.conn.WriteTo(requestBuf.Bytes(), destAddr); err != nil {
return nil, err
} else if n < len(requestBuf.Bytes()) {
return nil, fmt.Errorf("httpu: wrote %d bytes rather than full %d in request",
n, len(requestBuf.Bytes()))
}
time.Sleep(5 * time.Millisecond)
}
// Await responses until timeout.
var responses []*http.Response
responseBytes := make([]byte, 2048)
for {
// 2048 bytes should be sufficient for most networks.
n, _, err := httpu.conn.ReadFrom(responseBytes)
if err != nil {
if err, ok := err.(net.Error); ok {
if err.Timeout() {
break
}
if err.Temporary() {
// Sleep in case this is a persistent error to avoid pegging CPU until deadline.
time.Sleep(10 * time.Millisecond)
continue
}
}
return nil, err
}
// Parse response.
response, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(responseBytes[:n])), req)
if err != nil {
log.Print("httpu: error while parsing response: %v", err)
continue
}
responses = append(responses, response)
}
return responses, err
}

View File

@ -0,0 +1,108 @@
package httpu
import (
"bufio"
"bytes"
"log"
"net"
"net/http"
"regexp"
)
const (
DefaultMaxMessageBytes = 2048
)
var (
trailingWhitespaceRx = regexp.MustCompile(" +\r\n")
crlf = []byte("\r\n")
)
// Handler is the interface by which received HTTPU messages are passed to
// handling code.
type Handler interface {
// ServeMessage is called for each HTTPU message received. peerAddr contains
// the address that the message was received from.
ServeMessage(r *http.Request)
}
// HandlerFunc is a function-to-Handler adapter.
type HandlerFunc func(r *http.Request)
func (f HandlerFunc) ServeMessage(r *http.Request) {
f(r)
}
// A Server defines parameters for running an HTTPU server.
type Server struct {
Addr string // UDP address to listen on
Multicast bool // Should listen for multicast?
Interface *net.Interface // Network interface to listen on for multicast, nil for default multicast interface
Handler Handler // handler to invoke
MaxMessageBytes int // maximum number of bytes to read from a packet, DefaultMaxMessageBytes if 0
}
// ListenAndServe listens on the UDP network address srv.Addr. If srv.Multicast
// is true, then a multicast UDP listener will be used on srv.Interface (or
// default interface if nil).
func (srv *Server) ListenAndServe() error {
var err error
var addr *net.UDPAddr
if addr, err = net.ResolveUDPAddr("udp", srv.Addr); err != nil {
log.Fatal(err)
}
var conn net.PacketConn
if srv.Multicast {
if conn, err = net.ListenMulticastUDP("udp", srv.Interface, addr); err != nil {
return err
}
} else {
if conn, err = net.ListenUDP("udp", addr); err != nil {
return err
}
}
return srv.Serve(conn)
}
// Serve messages received on the given packet listener to the srv.Handler.
func (srv *Server) Serve(l net.PacketConn) error {
maxMessageBytes := DefaultMaxMessageBytes
if srv.MaxMessageBytes != 0 {
maxMessageBytes = srv.MaxMessageBytes
}
for {
buf := make([]byte, maxMessageBytes)
n, peerAddr, err := l.ReadFrom(buf)
if err != nil {
return err
}
buf = buf[:n]
go func(buf []byte, peerAddr net.Addr) {
// At least one router's UPnP implementation has added a trailing space
// after "HTTP/1.1" - trim it.
buf = trailingWhitespaceRx.ReplaceAllLiteral(buf, crlf)
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(buf)))
if err != nil {
log.Printf("httpu: Failed to parse request: %v", err)
return
}
req.RemoteAddr = peerAddr.String()
srv.Handler.ServeMessage(req)
// No need to call req.Body.Close - underlying reader is bytes.Buffer.
}(buf, peerAddr)
}
}
// Serve messages received on the given packet listener to the given handler.
func Serve(l net.PacketConn, handler Handler) error {
srv := Server{
Handler: handler,
MaxMessageBytes: DefaultMaxMessageBytes,
}
return srv.Serve(l)
}

View File

@ -0,0 +1,167 @@
package scpd
import (
"encoding/xml"
"strings"
)
const (
SCPDXMLNamespace = "urn:schemas-upnp-org:service-1-0"
)
func cleanWhitespace(s *string) {
*s = strings.TrimSpace(*s)
}
// SCPD is the service description as described by section 2.5 "Service
// description" in
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
type SCPD struct {
XMLName xml.Name `xml:"scpd"`
ConfigId string `xml:"configId,attr"`
SpecVersion SpecVersion `xml:"specVersion"`
Actions []Action `xml:"actionList>action"`
StateVariables []StateVariable `xml:"serviceStateTable>stateVariable"`
}
// Clean attempts to remove stray whitespace etc. in the structure. It seems
// unfortunately common for stray whitespace to be present in SCPD documents,
// this method attempts to make it easy to clean them out.
func (scpd *SCPD) Clean() {
cleanWhitespace(&scpd.ConfigId)
for i := range scpd.Actions {
scpd.Actions[i].clean()
}
for i := range scpd.StateVariables {
scpd.StateVariables[i].clean()
}
}
func (scpd *SCPD) GetStateVariable(variable string) *StateVariable {
for i := range scpd.StateVariables {
v := &scpd.StateVariables[i]
if v.Name == variable {
return v
}
}
return nil
}
func (scpd *SCPD) GetAction(action string) *Action {
for i := range scpd.Actions {
a := &scpd.Actions[i]
if a.Name == action {
return a
}
}
return nil
}
// SpecVersion is part of a SCPD document, describes the version of the
// specification that the data adheres to.
type SpecVersion struct {
Major int32 `xml:"major"`
Minor int32 `xml:"minor"`
}
type Action struct {
Name string `xml:"name"`
Arguments []Argument `xml:"argumentList>argument"`
}
func (action *Action) clean() {
cleanWhitespace(&action.Name)
for i := range action.Arguments {
action.Arguments[i].clean()
}
}
func (action *Action) InputArguments() []*Argument {
var result []*Argument
for i := range action.Arguments {
arg := &action.Arguments[i]
if arg.IsInput() {
result = append(result, arg)
}
}
return result
}
func (action *Action) OutputArguments() []*Argument {
var result []*Argument
for i := range action.Arguments {
arg := &action.Arguments[i]
if arg.IsOutput() {
result = append(result, arg)
}
}
return result
}
type Argument struct {
Name string `xml:"name"`
Direction string `xml:"direction"` // in|out
RelatedStateVariable string `xml:"relatedStateVariable"` // ?
Retval string `xml:"retval"` // ?
}
func (arg *Argument) clean() {
cleanWhitespace(&arg.Name)
cleanWhitespace(&arg.Direction)
cleanWhitespace(&arg.RelatedStateVariable)
cleanWhitespace(&arg.Retval)
}
func (arg *Argument) IsInput() bool {
return arg.Direction == "in"
}
func (arg *Argument) IsOutput() bool {
return arg.Direction == "out"
}
type StateVariable struct {
Name string `xml:"name"`
SendEvents string `xml:"sendEvents,attr"` // yes|no
Multicast string `xml:"multicast,attr"` // yes|no
DataType DataType `xml:"dataType"`
DefaultValue string `xml:"defaultValue"`
AllowedValueRange *AllowedValueRange `xml:"allowedValueRange"`
AllowedValues []string `xml:"allowedValueList>allowedValue"`
}
func (v *StateVariable) clean() {
cleanWhitespace(&v.Name)
cleanWhitespace(&v.SendEvents)
cleanWhitespace(&v.Multicast)
v.DataType.clean()
cleanWhitespace(&v.DefaultValue)
if v.AllowedValueRange != nil {
v.AllowedValueRange.clean()
}
for i := range v.AllowedValues {
cleanWhitespace(&v.AllowedValues[i])
}
}
type AllowedValueRange struct {
Minimum string `xml:"minimum"`
Maximum string `xml:"maximum"`
Step string `xml:"step"`
}
func (r *AllowedValueRange) clean() {
cleanWhitespace(&r.Minimum)
cleanWhitespace(&r.Maximum)
cleanWhitespace(&r.Step)
}
type DataType struct {
Name string `xml:",chardata"`
Type string `xml:"type,attr"`
}
func (dt *DataType) clean() {
cleanWhitespace(&dt.Name)
cleanWhitespace(&dt.Type)
}

View File

@ -0,0 +1,56 @@
package goupnp
import (
"fmt"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/soap"
)
// ServiceClient is a SOAP client, root device and the service for the SOAP
// client rolled into one value. The root device and service are intended to be
// informational.
type ServiceClient struct {
SOAPClient *soap.SOAPClient
RootDevice *RootDevice
Service *Service
}
func NewServiceClients(searchTarget string) (clients []ServiceClient, errors []error, err error) {
var maybeRootDevices []MaybeRootDevice
if maybeRootDevices, err = DiscoverDevices(searchTarget); err != nil {
return
}
clients = make([]ServiceClient, 0, len(maybeRootDevices))
for _, maybeRootDevice := range maybeRootDevices {
if maybeRootDevice.Err != nil {
errors = append(errors, maybeRootDevice.Err)
continue
}
device := &maybeRootDevice.Root.Device
srvs := device.FindService(searchTarget)
if len(srvs) == 0 {
errors = append(errors, fmt.Errorf("goupnp: service %q not found within device %q (UDN=%q)",
searchTarget, device.FriendlyName, device.UDN))
continue
}
for _, srv := range srvs {
clients = append(clients, ServiceClient{
SOAPClient: srv.NewSOAPClient(),
RootDevice: maybeRootDevice.Root,
Service: srv,
})
}
}
return
}
// GetServiceClient returns the ServiceClient itself. This is provided so that the
// service client attributes can be accessed via an interface method on a
// wrapping type.
func (client *ServiceClient) GetServiceClient() *ServiceClient {
return client
}

View File

@ -0,0 +1,157 @@
// Definition for the SOAP structure required for UPnP's SOAP usage.
package soap
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
)
const (
soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
soapPrefix = xml.Header + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`
soapSuffix = `</s:Body></s:Envelope>`
)
type SOAPClient struct {
EndpointURL url.URL
HTTPClient http.Client
}
func NewSOAPClient(endpointURL url.URL) *SOAPClient {
return &SOAPClient{
EndpointURL: endpointURL,
}
}
// PerformSOAPAction makes a SOAP request, with the given action.
// inAction and outAction must both be pointers to structs with string fields
// only.
func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAction interface{}, outAction interface{}) error {
requestBytes, err := encodeRequestAction(actionNamespace, actionName, inAction)
if err != nil {
return err
}
response, err := client.HTTPClient.Do(&http.Request{
Method: "POST",
URL: &client.EndpointURL,
Header: http.Header{
"SOAPACTION": []string{actionNamespace + "#" + actionName},
"CONTENT-TYPE": []string{"text/xml; charset=\"utf-8\""},
},
Body: ioutil.NopCloser(bytes.NewBuffer(requestBytes)),
// Set ContentLength to avoid chunked encoding - some servers might not support it.
ContentLength: int64(len(requestBytes)),
})
if err != nil {
return fmt.Errorf("goupnp: error performing SOAP HTTP request: %v", err)
}
defer response.Body.Close()
if response.StatusCode != 200 {
return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
}
responseEnv := newSOAPEnvelope()
decoder := xml.NewDecoder(response.Body)
if err := decoder.Decode(responseEnv); err != nil {
return fmt.Errorf("goupnp: error decoding response body: %v", err)
}
if responseEnv.Body.Fault != nil {
return responseEnv.Body.Fault
}
if outAction != nil {
if err := xml.Unmarshal(responseEnv.Body.RawAction, outAction); err != nil {
return fmt.Errorf("goupnp: error unmarshalling out action: %v, %v", err, responseEnv.Body.RawAction)
}
}
return nil
}
// newSOAPAction creates a soapEnvelope with the given action and arguments.
func newSOAPEnvelope() *soapEnvelope {
return &soapEnvelope{
EncodingStyle: soapEncodingStyle,
}
}
// encodeRequestAction is a hacky way to create an encoded SOAP envelope
// containing the given action. Experiments with one router have shown that it
// 500s for requests where the outer default xmlns is set to the SOAP
// namespace, and then reassigning the default namespace within that to the
// service namespace. Hand-coding the outer XML to work-around this.
func encodeRequestAction(actionNamespace, actionName string, inAction interface{}) ([]byte, error) {
requestBuf := new(bytes.Buffer)
requestBuf.WriteString(soapPrefix)
requestBuf.WriteString(`<u:`)
xml.EscapeText(requestBuf, []byte(actionName))
requestBuf.WriteString(` xmlns:u="`)
xml.EscapeText(requestBuf, []byte(actionNamespace))
requestBuf.WriteString(`">`)
if inAction != nil {
if err := encodeRequestArgs(requestBuf, inAction); err != nil {
return nil, err
}
}
requestBuf.WriteString(`</u:`)
xml.EscapeText(requestBuf, []byte(actionName))
requestBuf.WriteString(`>`)
requestBuf.WriteString(soapSuffix)
return requestBuf.Bytes(), nil
}
func encodeRequestArgs(w *bytes.Buffer, inAction interface{}) error {
in := reflect.Indirect(reflect.ValueOf(inAction))
if in.Kind() != reflect.Struct {
return fmt.Errorf("goupnp: SOAP inAction is not a struct but of type %v", in.Type())
}
enc := xml.NewEncoder(w)
nFields := in.NumField()
inType := in.Type()
for i := 0; i < nFields; i++ {
field := inType.Field(i)
argName := field.Name
if nameOverride := field.Tag.Get("soap"); nameOverride != "" {
argName = nameOverride
}
value := in.Field(i)
if value.Kind() != reflect.String {
return fmt.Errorf("goupnp: SOAP arg %q is not of type string, but of type %v", argName, value.Type())
}
if err := enc.EncodeElement(value.Interface(), xml.StartElement{xml.Name{"", argName}, nil}); err != nil {
return fmt.Errorf("goupnp: error encoding SOAP arg %q: %v", argName, err)
}
}
enc.Flush()
return nil
}
type soapEnvelope struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
EncodingStyle string `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"`
Body soapBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
}
type soapBody struct {
Fault *SOAPFaultError `xml:"Fault"`
RawAction []byte `xml:",innerxml"`
}
// SOAPFaultError implements error, and contains SOAP fault information.
type SOAPFaultError struct {
FaultCode string `xml:"faultcode"`
FaultString string `xml:"faultstring"`
Detail string `xml:"detail"`
}
func (err *SOAPFaultError) Error() string {
return fmt.Sprintf("SOAP fault: %s", err.FaultString)
}

View File

@ -0,0 +1,85 @@
package soap
import (
"bytes"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"testing"
)
type capturingRoundTripper struct {
err error
resp *http.Response
capturedReq *http.Request
}
func (rt *capturingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
rt.capturedReq = req
return rt.resp, rt.err
}
func TestActionInputs(t *testing.T) {
url, err := url.Parse("http://example.com/soap")
if err != nil {
t.Fatal(err)
}
rt := &capturingRoundTripper{
err: nil,
resp: &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(`
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:myactionResponse xmlns:u="mynamespace">
<A>valueA</A>
<B>valueB</B>
</u:myactionResponse>
</s:Body>
</s:Envelope>
`)),
},
}
client := SOAPClient{
EndpointURL: *url,
HTTPClient: http.Client{
Transport: rt,
},
}
type In struct {
Foo string
Bar string `soap:"bar"`
}
type Out struct {
A string
B string
}
in := In{"foo", "bar"}
gotOut := Out{}
err = client.PerformAction("mynamespace", "myaction", &in, &gotOut)
if err != nil {
t.Fatal(err)
}
wantBody := (soapPrefix +
`<u:myaction xmlns:u="mynamespace">` +
`<Foo>foo</Foo>` +
`<bar>bar</bar>` +
`</u:myaction>` +
soapSuffix)
body, err := ioutil.ReadAll(rt.capturedReq.Body)
if err != nil {
t.Fatal(err)
}
gotBody := string(body)
if wantBody != gotBody {
t.Errorf("Bad request body\nwant: %q\n got: %q", wantBody, gotBody)
}
wantOut := Out{"valueA", "valueB"}
if !reflect.DeepEqual(wantOut, gotOut) {
t.Errorf("Bad output\nwant: %+v\n got: %+v", wantOut, gotOut)
}
}

View File

@ -0,0 +1,508 @@
package soap
import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
)
var (
// localLoc acts like time.Local for this package, but is faked out by the
// unit tests to ensure that things stay constant (especially when running
// this test in a place where local time is UTC which might mask bugs).
localLoc = time.Local
)
func MarshalUi1(v uint8) (string, error) {
return strconv.FormatUint(uint64(v), 10), nil
}
func UnmarshalUi1(s string) (uint8, error) {
v, err := strconv.ParseUint(s, 10, 8)
return uint8(v), err
}
func MarshalUi2(v uint16) (string, error) {
return strconv.FormatUint(uint64(v), 10), nil
}
func UnmarshalUi2(s string) (uint16, error) {
v, err := strconv.ParseUint(s, 10, 16)
return uint16(v), err
}
func MarshalUi4(v uint32) (string, error) {
return strconv.FormatUint(uint64(v), 10), nil
}
func UnmarshalUi4(s string) (uint32, error) {
v, err := strconv.ParseUint(s, 10, 32)
return uint32(v), err
}
func MarshalI1(v int8) (string, error) {
return strconv.FormatInt(int64(v), 10), nil
}
func UnmarshalI1(s string) (int8, error) {
v, err := strconv.ParseInt(s, 10, 8)
return int8(v), err
}
func MarshalI2(v int16) (string, error) {
return strconv.FormatInt(int64(v), 10), nil
}
func UnmarshalI2(s string) (int16, error) {
v, err := strconv.ParseInt(s, 10, 16)
return int16(v), err
}
func MarshalI4(v int32) (string, error) {
return strconv.FormatInt(int64(v), 10), nil
}
func UnmarshalI4(s string) (int32, error) {
v, err := strconv.ParseInt(s, 10, 32)
return int32(v), err
}
func MarshalInt(v int64) (string, error) {
return strconv.FormatInt(v, 10), nil
}
func UnmarshalInt(s string) (int64, error) {
return strconv.ParseInt(s, 10, 64)
}
func MarshalR4(v float32) (string, error) {
return strconv.FormatFloat(float64(v), 'G', -1, 32), nil
}
func UnmarshalR4(s string) (float32, error) {
v, err := strconv.ParseFloat(s, 32)
return float32(v), err
}
func MarshalR8(v float64) (string, error) {
return strconv.FormatFloat(v, 'G', -1, 64), nil
}
func UnmarshalR8(s string) (float64, error) {
v, err := strconv.ParseFloat(s, 64)
return float64(v), err
}
// MarshalFixed14_4 marshals float64 to SOAP "fixed.14.4" type.
func MarshalFixed14_4(v float64) (string, error) {
if v >= 1e14 || v <= -1e14 {
return "", fmt.Errorf("soap fixed14.4: value %v out of bounds", v)
}
return strconv.FormatFloat(v, 'f', 4, 64), nil
}
// UnmarshalFixed14_4 unmarshals float64 from SOAP "fixed.14.4" type.
func UnmarshalFixed14_4(s string) (float64, error) {
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, err
}
if v >= 1e14 || v <= -1e14 {
return 0, fmt.Errorf("soap fixed14.4: value %q out of bounds", s)
}
return v, nil
}
// MarshalChar marshals rune to SOAP "char" type.
func MarshalChar(v rune) (string, error) {
if v == 0 {
return "", errors.New("soap char: rune 0 is not allowed")
}
return string(v), nil
}
// UnmarshalChar unmarshals rune from SOAP "char" type.
func UnmarshalChar(s string) (rune, error) {
if len(s) == 0 {
return 0, errors.New("soap char: got empty string")
}
r, n := utf8.DecodeRune([]byte(s))
if n != len(s) {
return 0, fmt.Errorf("soap char: value %q is not a single rune", s)
}
return r, nil
}
func MarshalString(v string) (string, error) {
return v, nil
}
func UnmarshalString(v string) (string, error) {
return v, nil
}
func parseInt(s string, err *error) int {
v, parseErr := strconv.ParseInt(s, 10, 64)
if parseErr != nil {
*err = parseErr
}
return int(v)
}
var dateRegexps = []*regexp.Regexp{
// yyyy[-mm[-dd]]
regexp.MustCompile(`^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$`),
// yyyy[mm[dd]]
regexp.MustCompile(`^(\d{4})(?:(\d{2})(?:(\d{2}))?)?$`),
}
func parseDateParts(s string) (year, month, day int, err error) {
var parts []string
for _, re := range dateRegexps {
parts = re.FindStringSubmatch(s)
if parts != nil {
break
}
}
if parts == nil {
err = fmt.Errorf("soap date: value %q is not in a recognized ISO8601 date format", s)
return
}
year = parseInt(parts[1], &err)
month = 1
day = 1
if len(parts[2]) != 0 {
month = parseInt(parts[2], &err)
if len(parts[3]) != 0 {
day = parseInt(parts[3], &err)
}
}
if err != nil {
err = fmt.Errorf("soap date: %q: %v", s, err)
}
return
}
var timeRegexps = []*regexp.Regexp{
// hh[:mm[:ss]]
regexp.MustCompile(`^(\d{2})(?::(\d{2})(?::(\d{2}))?)?$`),
// hh[mm[ss]]
regexp.MustCompile(`^(\d{2})(?:(\d{2})(?:(\d{2}))?)?$`),
}
func parseTimeParts(s string) (hour, minute, second int, err error) {
var parts []string
for _, re := range timeRegexps {
parts = re.FindStringSubmatch(s)
if parts != nil {
break
}
}
if parts == nil {
err = fmt.Errorf("soap time: value %q is not in ISO8601 time format", s)
return
}
hour = parseInt(parts[1], &err)
if len(parts[2]) != 0 {
minute = parseInt(parts[2], &err)
if len(parts[3]) != 0 {
second = parseInt(parts[3], &err)
}
}
if err != nil {
err = fmt.Errorf("soap time: %q: %v", s, err)
}
return
}
// (+|-)hh[[:]mm]
var timezoneRegexp = regexp.MustCompile(`^([+-])(\d{2})(?::?(\d{2}))?$`)
func parseTimezone(s string) (offset int, err error) {
if s == "Z" {
return 0, nil
}
parts := timezoneRegexp.FindStringSubmatch(s)
if parts == nil {
err = fmt.Errorf("soap timezone: value %q is not in ISO8601 timezone format", s)
return
}
offset = parseInt(parts[2], &err) * 3600
if len(parts[3]) != 0 {
offset += parseInt(parts[3], &err) * 60
}
if parts[1] == "-" {
offset = -offset
}
if err != nil {
err = fmt.Errorf("soap timezone: %q: %v", s, err)
}
return
}
var completeDateTimeZoneRegexp = regexp.MustCompile(`^([^T]+)(?:T([^-+Z]+)(.+)?)?$`)
// splitCompleteDateTimeZone splits date, time and timezone apart from an
// ISO8601 string. It does not ensure that the contents of each part are
// correct, it merely splits on certain delimiters.
// e.g "2010-09-08T12:15:10+0700" => "2010-09-08", "12:15:10", "+0700".
// Timezone can only be present if time is also present.
func splitCompleteDateTimeZone(s string) (dateStr, timeStr, zoneStr string, err error) {
parts := completeDateTimeZoneRegexp.FindStringSubmatch(s)
if parts == nil {
err = fmt.Errorf("soap date/time/zone: value %q is not in ISO8601 datetime format", s)
return
}
dateStr = parts[1]
timeStr = parts[2]
zoneStr = parts[3]
return
}
// MarshalDate marshals time.Time to SOAP "date" type. Note that this converts
// to local time, and discards the time-of-day components.
func MarshalDate(v time.Time) (string, error) {
return v.In(localLoc).Format("2006-01-02"), nil
}
var dateFmts = []string{"2006-01-02", "20060102"}
// UnmarshalDate unmarshals time.Time from SOAP "date" type. This outputs the
// date as midnight in the local time zone.
func UnmarshalDate(s string) (time.Time, error) {
year, month, day, err := parseDateParts(s)
if err != nil {
return time.Time{}, err
}
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, localLoc), nil
}
// TimeOfDay is used in cases where SOAP "time" or "time.tz" is used.
type TimeOfDay struct {
// Duration of time since midnight.
FromMidnight time.Duration
// Set to true if Offset is specified. If false, then the timezone is
// unspecified (and by ISO8601 - implies some "local" time).
HasOffset bool
// Offset is non-zero only if time.tz is used. It is otherwise ignored. If
// non-zero, then it is regarded as a UTC offset in seconds. Note that the
// sub-minutes is ignored by the marshal function.
Offset int
}
// MarshalTimeOfDay marshals TimeOfDay to the "time" type.
func MarshalTimeOfDay(v TimeOfDay) (string, error) {
d := int64(v.FromMidnight / time.Second)
hour := d / 3600
d = d % 3600
minute := d / 60
second := d % 60
return fmt.Sprintf("%02d:%02d:%02d", hour, minute, second), nil
}
// UnmarshalTimeOfDay unmarshals TimeOfDay from the "time" type.
func UnmarshalTimeOfDay(s string) (TimeOfDay, error) {
t, err := UnmarshalTimeOfDayTz(s)
if err != nil {
return TimeOfDay{}, err
} else if t.HasOffset {
return TimeOfDay{}, fmt.Errorf("soap time: value %q contains unexpected timezone")
}
return t, nil
}
// MarshalTimeOfDayTz marshals TimeOfDay to the "time.tz" type.
func MarshalTimeOfDayTz(v TimeOfDay) (string, error) {
d := int64(v.FromMidnight / time.Second)
hour := d / 3600
d = d % 3600
minute := d / 60
second := d % 60
tz := ""
if v.HasOffset {
if v.Offset == 0 {
tz = "Z"
} else {
offsetMins := v.Offset / 60
sign := '+'
if offsetMins < 1 {
offsetMins = -offsetMins
sign = '-'
}
tz = fmt.Sprintf("%c%02d:%02d", sign, offsetMins/60, offsetMins%60)
}
}
return fmt.Sprintf("%02d:%02d:%02d%s", hour, minute, second, tz), nil
}
// UnmarshalTimeOfDayTz unmarshals TimeOfDay from the "time.tz" type.
func UnmarshalTimeOfDayTz(s string) (tod TimeOfDay, err error) {
zoneIndex := strings.IndexAny(s, "Z+-")
var timePart string
var hasOffset bool
var offset int
if zoneIndex == -1 {
hasOffset = false
timePart = s
} else {
hasOffset = true
timePart = s[:zoneIndex]
if offset, err = parseTimezone(s[zoneIndex:]); err != nil {
return
}
}
hour, minute, second, err := parseTimeParts(timePart)
if err != nil {
return
}
fromMidnight := time.Duration(hour*3600+minute*60+second) * time.Second
// ISO8601 special case - values up to 24:00:00 are allowed, so using
// strictly greater-than for the maximum value.
if fromMidnight > 24*time.Hour || minute >= 60 || second >= 60 {
return TimeOfDay{}, fmt.Errorf("soap time.tz: value %q has value(s) out of range", s)
}
return TimeOfDay{
FromMidnight: time.Duration(hour*3600+minute*60+second) * time.Second,
HasOffset: hasOffset,
Offset: offset,
}, nil
}
// MarshalDateTime marshals time.Time to SOAP "dateTime" type. Note that this
// converts to local time.
func MarshalDateTime(v time.Time) (string, error) {
return v.In(localLoc).Format("2006-01-02T15:04:05"), nil
}
// UnmarshalDateTime unmarshals time.Time from the SOAP "dateTime" type. This
// returns a value in the local timezone.
func UnmarshalDateTime(s string) (result time.Time, err error) {
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
if err != nil {
return
}
if len(zoneStr) != 0 {
err = fmt.Errorf("soap datetime: unexpected timezone in %q", s)
return
}
year, month, day, err := parseDateParts(dateStr)
if err != nil {
return
}
var hour, minute, second int
if len(timeStr) != 0 {
hour, minute, second, err = parseTimeParts(timeStr)
if err != nil {
return
}
}
result = time.Date(year, time.Month(month), day, hour, minute, second, 0, localLoc)
return
}
// MarshalDateTimeTz marshals time.Time to SOAP "dateTime.tz" type.
func MarshalDateTimeTz(v time.Time) (string, error) {
return v.Format("2006-01-02T15:04:05-07:00"), nil
}
// UnmarshalDateTimeTz unmarshals time.Time from the SOAP "dateTime.tz" type.
// This returns a value in the local timezone when the timezone is unspecified.
func UnmarshalDateTimeTz(s string) (result time.Time, err error) {
dateStr, timeStr, zoneStr, err := splitCompleteDateTimeZone(s)
if err != nil {
return
}
year, month, day, err := parseDateParts(dateStr)
if err != nil {
return
}
var hour, minute, second int
var location *time.Location = localLoc
if len(timeStr) != 0 {
hour, minute, second, err = parseTimeParts(timeStr)
if err != nil {
return
}
if len(zoneStr) != 0 {
var offset int
offset, err = parseTimezone(zoneStr)
if offset == 0 {
location = time.UTC
} else {
location = time.FixedZone("", offset)
}
}
}
result = time.Date(year, time.Month(month), day, hour, minute, second, 0, location)
return
}
// MarshalBoolean marshals bool to SOAP "boolean" type.
func MarshalBoolean(v bool) (string, error) {
if v {
return "1", nil
}
return "0", nil
}
// UnmarshalBoolean unmarshals bool from the SOAP "boolean" type.
func UnmarshalBoolean(s string) (bool, error) {
switch s {
case "0", "false", "no":
return false, nil
case "1", "true", "yes":
return true, nil
}
return false, fmt.Errorf("soap boolean: %q is not a valid boolean value", s)
}
// MarshalBinBase64 marshals []byte to SOAP "bin.base64" type.
func MarshalBinBase64(v []byte) (string, error) {
return base64.StdEncoding.EncodeToString(v), nil
}
// UnmarshalBinBase64 unmarshals []byte from the SOAP "bin.base64" type.
func UnmarshalBinBase64(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}
// MarshalBinHex marshals []byte to SOAP "bin.hex" type.
func MarshalBinHex(v []byte) (string, error) {
return hex.EncodeToString(v), nil
}
// UnmarshalBinHex unmarshals []byte from the SOAP "bin.hex" type.
func UnmarshalBinHex(s string) ([]byte, error) {
return hex.DecodeString(s)
}

View File

@ -0,0 +1,481 @@
package soap
import (
"bytes"
"math"
"testing"
"time"
)
type convTest interface {
Marshal() (string, error)
Unmarshal(string) (interface{}, error)
Equal(result interface{}) bool
}
// duper is an interface that convTest values may optionally also implement to
// generate another convTest for a value in an otherwise identical testCase.
type duper interface {
Dupe(tag string) []convTest
}
type testCase struct {
value convTest
str string
wantMarshalErr bool
wantUnmarshalErr bool
noMarshal bool
noUnMarshal bool
tag string
}
type Ui1Test uint8
func (v Ui1Test) Marshal() (string, error) {
return MarshalUi1(uint8(v))
}
func (v Ui1Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalUi1(s)
}
func (v Ui1Test) Equal(result interface{}) bool {
return uint8(v) == result.(uint8)
}
func (v Ui1Test) Dupe(tag string) []convTest {
if tag == "dupe" {
return []convTest{
Ui2Test(v),
Ui4Test(v),
}
}
return nil
}
type Ui2Test uint16
func (v Ui2Test) Marshal() (string, error) {
return MarshalUi2(uint16(v))
}
func (v Ui2Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalUi2(s)
}
func (v Ui2Test) Equal(result interface{}) bool {
return uint16(v) == result.(uint16)
}
type Ui4Test uint32
func (v Ui4Test) Marshal() (string, error) {
return MarshalUi4(uint32(v))
}
func (v Ui4Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalUi4(s)
}
func (v Ui4Test) Equal(result interface{}) bool {
return uint32(v) == result.(uint32)
}
type I1Test int8
func (v I1Test) Marshal() (string, error) {
return MarshalI1(int8(v))
}
func (v I1Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalI1(s)
}
func (v I1Test) Equal(result interface{}) bool {
return int8(v) == result.(int8)
}
func (v I1Test) Dupe(tag string) []convTest {
if tag == "dupe" {
return []convTest{
I2Test(v),
I4Test(v),
}
}
return nil
}
type I2Test int16
func (v I2Test) Marshal() (string, error) {
return MarshalI2(int16(v))
}
func (v I2Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalI2(s)
}
func (v I2Test) Equal(result interface{}) bool {
return int16(v) == result.(int16)
}
type I4Test int32
func (v I4Test) Marshal() (string, error) {
return MarshalI4(int32(v))
}
func (v I4Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalI4(s)
}
func (v I4Test) Equal(result interface{}) bool {
return int32(v) == result.(int32)
}
type IntTest int64
func (v IntTest) Marshal() (string, error) {
return MarshalInt(int64(v))
}
func (v IntTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalInt(s)
}
func (v IntTest) Equal(result interface{}) bool {
return int64(v) == result.(int64)
}
type Fixed14_4Test float64
func (v Fixed14_4Test) Marshal() (string, error) {
return MarshalFixed14_4(float64(v))
}
func (v Fixed14_4Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalFixed14_4(s)
}
func (v Fixed14_4Test) Equal(result interface{}) bool {
return math.Abs(float64(v)-result.(float64)) < 0.001
}
type CharTest rune
func (v CharTest) Marshal() (string, error) {
return MarshalChar(rune(v))
}
func (v CharTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalChar(s)
}
func (v CharTest) Equal(result interface{}) bool {
return rune(v) == result.(rune)
}
type DateTest struct{ time.Time }
func (v DateTest) Marshal() (string, error) {
return MarshalDate(time.Time(v.Time))
}
func (v DateTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDate(s)
}
func (v DateTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
func (v DateTest) Dupe(tag string) []convTest {
if tag != "no:dateTime" {
return []convTest{DateTimeTest{v.Time}}
}
return nil
}
type TimeOfDayTest struct {
TimeOfDay
}
func (v TimeOfDayTest) Marshal() (string, error) {
return MarshalTimeOfDay(v.TimeOfDay)
}
func (v TimeOfDayTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalTimeOfDay(s)
}
func (v TimeOfDayTest) Equal(result interface{}) bool {
return v.TimeOfDay == result.(TimeOfDay)
}
func (v TimeOfDayTest) Dupe(tag string) []convTest {
if tag != "no:time.tz" {
return []convTest{TimeOfDayTzTest{v.TimeOfDay}}
}
return nil
}
type TimeOfDayTzTest struct {
TimeOfDay
}
func (v TimeOfDayTzTest) Marshal() (string, error) {
return MarshalTimeOfDayTz(v.TimeOfDay)
}
func (v TimeOfDayTzTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalTimeOfDayTz(s)
}
func (v TimeOfDayTzTest) Equal(result interface{}) bool {
return v.TimeOfDay == result.(TimeOfDay)
}
type DateTimeTest struct{ time.Time }
func (v DateTimeTest) Marshal() (string, error) {
return MarshalDateTime(time.Time(v.Time))
}
func (v DateTimeTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDateTime(s)
}
func (v DateTimeTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
func (v DateTimeTest) Dupe(tag string) []convTest {
if tag != "no:dateTime.tz" {
return []convTest{DateTimeTzTest{v.Time}}
}
return nil
}
type DateTimeTzTest struct{ time.Time }
func (v DateTimeTzTest) Marshal() (string, error) {
return MarshalDateTimeTz(time.Time(v.Time))
}
func (v DateTimeTzTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalDateTimeTz(s)
}
func (v DateTimeTzTest) Equal(result interface{}) bool {
return v.Time.Equal(result.(time.Time))
}
type BooleanTest bool
func (v BooleanTest) Marshal() (string, error) {
return MarshalBoolean(bool(v))
}
func (v BooleanTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalBoolean(s)
}
func (v BooleanTest) Equal(result interface{}) bool {
return bool(v) == result.(bool)
}
type BinBase64Test []byte
func (v BinBase64Test) Marshal() (string, error) {
return MarshalBinBase64([]byte(v))
}
func (v BinBase64Test) Unmarshal(s string) (interface{}, error) {
return UnmarshalBinBase64(s)
}
func (v BinBase64Test) Equal(result interface{}) bool {
return bytes.Equal([]byte(v), result.([]byte))
}
type BinHexTest []byte
func (v BinHexTest) Marshal() (string, error) {
return MarshalBinHex([]byte(v))
}
func (v BinHexTest) Unmarshal(s string) (interface{}, error) {
return UnmarshalBinHex(s)
}
func (v BinHexTest) Equal(result interface{}) bool {
return bytes.Equal([]byte(v), result.([]byte))
}
func Test(t *testing.T) {
const time010203 time.Duration = (1*3600 + 2*60 + 3) * time.Second
const time0102 time.Duration = (1*3600 + 2*60) * time.Second
const time01 time.Duration = (1 * 3600) * time.Second
const time235959 time.Duration = (23*3600 + 59*60 + 59) * time.Second
// Fake out the local time for the implementation.
localLoc = time.FixedZone("Fake/Local", 6*3600)
defer func() {
localLoc = time.Local
}()
tests := []testCase{
// ui1
{str: "", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: " ", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "abc", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "-1", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "0", value: Ui1Test(0), tag: "dupe"},
{str: "1", value: Ui1Test(1), tag: "dupe"},
{str: "255", value: Ui1Test(255), tag: "dupe"},
{str: "256", value: Ui1Test(0), wantUnmarshalErr: true, noMarshal: true},
// ui2
{str: "65535", value: Ui2Test(65535)},
{str: "65536", value: Ui2Test(0), wantUnmarshalErr: true, noMarshal: true},
// ui4
{str: "4294967295", value: Ui4Test(4294967295)},
{str: "4294967296", value: Ui4Test(0), wantUnmarshalErr: true, noMarshal: true},
// i1
{str: "", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: " ", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "abc", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true, tag: "dupe"},
{str: "0", value: I1Test(0), tag: "dupe"},
{str: "-1", value: I1Test(-1), tag: "dupe"},
{str: "127", value: I1Test(127), tag: "dupe"},
{str: "-128", value: I1Test(-128), tag: "dupe"},
{str: "128", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-129", value: I1Test(0), wantUnmarshalErr: true, noMarshal: true},
// i2
{str: "32767", value: I2Test(32767)},
{str: "-32768", value: I2Test(-32768)},
{str: "32768", value: I2Test(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-32769", value: I2Test(0), wantUnmarshalErr: true, noMarshal: true},
// i4
{str: "2147483647", value: I4Test(2147483647)},
{str: "-2147483648", value: I4Test(-2147483648)},
{str: "2147483648", value: I4Test(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-2147483649", value: I4Test(0), wantUnmarshalErr: true, noMarshal: true},
// int
{str: "9223372036854775807", value: IntTest(9223372036854775807)},
{str: "-9223372036854775808", value: IntTest(-9223372036854775808)},
{str: "9223372036854775808", value: IntTest(0), wantUnmarshalErr: true, noMarshal: true},
{str: "-9223372036854775809", value: IntTest(0), wantUnmarshalErr: true, noMarshal: true},
// fixed.14.4
{str: "0.0000", value: Fixed14_4Test(0)},
{str: "1.0000", value: Fixed14_4Test(1)},
{str: "1.2346", value: Fixed14_4Test(1.23456)},
{str: "-1.0000", value: Fixed14_4Test(-1)},
{str: "-1.2346", value: Fixed14_4Test(-1.23456)},
{str: "10000000000000.0000", value: Fixed14_4Test(1e13)},
{str: "100000000000000.0000", value: Fixed14_4Test(1e14), wantMarshalErr: true, wantUnmarshalErr: true},
{str: "-10000000000000.0000", value: Fixed14_4Test(-1e13)},
{str: "-100000000000000.0000", value: Fixed14_4Test(-1e14), wantMarshalErr: true, wantUnmarshalErr: true},
// char
{str: "a", value: CharTest('a')},
{str: "z", value: CharTest('z')},
{str: "\u1234", value: CharTest(0x1234)},
{str: "aa", value: CharTest(0), wantMarshalErr: true, wantUnmarshalErr: true},
{str: "", value: CharTest(0), wantMarshalErr: true, wantUnmarshalErr: true},
// date
{str: "2013-10-08", value: DateTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime"},
{str: "20131008", value: DateTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true, tag: "no:dateTime"},
{str: "2013-10-08T10:30:50", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime"},
{str: "2013-10-08T10:30:50Z", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime"},
{str: "", value: DateTest{}, wantMarshalErr: true, wantUnmarshalErr: true, noMarshal: true},
{str: "-1", value: DateTest{}, wantUnmarshalErr: true, noMarshal: true},
// time
{str: "00:00:00", value: TimeOfDayTest{TimeOfDay{FromMidnight: 0}}},
{str: "000000", value: TimeOfDayTest{TimeOfDay{FromMidnight: 0}}, noMarshal: true},
{str: "24:00:00", value: TimeOfDayTest{TimeOfDay{FromMidnight: 24 * time.Hour}}, noMarshal: true}, // ISO8601 special case
{str: "24:01:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "24:00:01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "25:00:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "00:60:00", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "00:00:60", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "01:02:03", value: TimeOfDayTest{TimeOfDay{FromMidnight: time010203}}},
{str: "010203", value: TimeOfDayTest{TimeOfDay{FromMidnight: time010203}}, noMarshal: true},
{str: "23:59:59", value: TimeOfDayTest{TimeOfDay{FromMidnight: time235959}}},
{str: "235959", value: TimeOfDayTest{TimeOfDay{FromMidnight: time235959}}, noMarshal: true},
{str: "01:02", value: TimeOfDayTest{TimeOfDay{FromMidnight: time0102}}, noMarshal: true},
{str: "0102", value: TimeOfDayTest{TimeOfDay{FromMidnight: time0102}}, noMarshal: true},
{str: "01", value: TimeOfDayTest{TimeOfDay{FromMidnight: time01}}, noMarshal: true},
{str: "foo 01:02:03", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "foo\n01:02:03", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03 foo", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03\nfoo", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03Z", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03+01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03+01:23", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03+0123", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03-01", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03-01:23", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
{str: "01:02:03-0123", value: TimeOfDayTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:time.tz"},
// time.tz
{str: "24:00:01", value: TimeOfDayTzTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "01Z", value: TimeOfDayTzTest{TimeOfDay{time01, true, 0}}, noMarshal: true},
{str: "01:02:03Z", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 0}}},
{str: "01+01", value: TimeOfDayTzTest{TimeOfDay{time01, true, 3600}}, noMarshal: true},
{str: "01:02:03+01", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600}}, noMarshal: true},
{str: "01:02:03+01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600 + 23*60}}},
{str: "01:02:03+0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, 3600 + 23*60}}, noMarshal: true},
{str: "01:02:03-01", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -3600}}, noMarshal: true},
{str: "01:02:03-01:23", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}},
{str: "01:02:03-0123", value: TimeOfDayTzTest{TimeOfDay{time010203, true, -(3600 + 23*60)}}, noMarshal: true},
// dateTime
{str: "2013-10-08T00:00:00", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, tag: "no:dateTime.tz"},
{str: "20131008", value: DateTimeTest{time.Date(2013, 10, 8, 0, 0, 0, 0, localLoc)}, noMarshal: true},
{str: "2013-10-08T10:30:50", value: DateTimeTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50T", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true},
{str: "2013-10-08T10:30:50+01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50+0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-01", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
{str: "2013-10-08T10:30:50-0123", value: DateTimeTest{}, wantUnmarshalErr: true, noMarshal: true, tag: "no:dateTime.tz"},
// dateTime.tz
{str: "2013-10-08T10:30:50", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, localLoc)}, noMarshal: true},
{str: "2013-10-08T10:30:50+01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:00", 3600))}, noMarshal: true},
{str: "2013-10-08T10:30:50+01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}},
{str: "2013-10-08T10:30:50+0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("+01:23", 3600+23*60))}, noMarshal: true},
{str: "2013-10-08T10:30:50-01", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:00", -3600))}, noMarshal: true},
{str: "2013-10-08T10:30:50-01:23", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}},
{str: "2013-10-08T10:30:50-0123", value: DateTimeTzTest{time.Date(2013, 10, 8, 10, 30, 50, 0, time.FixedZone("-01:23", -(3600+23*60)))}, noMarshal: true},
// boolean
{str: "0", value: BooleanTest(false)},
{str: "1", value: BooleanTest(true)},
{str: "false", value: BooleanTest(false), noMarshal: true},
{str: "true", value: BooleanTest(true), noMarshal: true},
{str: "no", value: BooleanTest(false), noMarshal: true},
{str: "yes", value: BooleanTest(true), noMarshal: true},
{str: "", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
{str: "other", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
{str: "2", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
{str: "-1", value: BooleanTest(false), noMarshal: true, wantUnmarshalErr: true},
// bin.base64
{str: "", value: BinBase64Test{}},
{str: "YQ==", value: BinBase64Test("a")},
{str: "TG9uZ2VyIFN0cmluZy4=", value: BinBase64Test("Longer String.")},
{str: "TG9uZ2VyIEFsaWduZWQu", value: BinBase64Test("Longer Aligned.")},
// bin.hex
{str: "", value: BinHexTest{}},
{str: "61", value: BinHexTest("a")},
{str: "4c6f6e67657220537472696e672e", value: BinHexTest("Longer String.")},
{str: "4C6F6E67657220537472696E672E", value: BinHexTest("Longer String."), noMarshal: true},
}
// Generate extra test cases from convTests that implement duper.
var extras []testCase
for i := range tests {
if duper, ok := tests[i].value.(duper); ok {
dupes := duper.Dupe(tests[i].tag)
for _, duped := range dupes {
dupedCase := testCase(tests[i])
dupedCase.value = duped
extras = append(extras, dupedCase)
}
}
}
tests = append(tests, extras...)
for _, test := range tests {
if test.noMarshal {
} else if resultStr, err := test.value.Marshal(); err != nil && !test.wantMarshalErr {
t.Errorf("For %T marshal %v, want %q, got error: %v", test.value, test.value, test.str, err)
} else if err == nil && test.wantMarshalErr {
t.Errorf("For %T marshal %v, want error, got %q", test.value, test.value, resultStr)
} else if err == nil && resultStr != test.str {
t.Errorf("For %T marshal %v, want %q, got %q", test.value, test.value, test.str, resultStr)
}
if test.noUnMarshal {
} else if resultValue, err := test.value.Unmarshal(test.str); err != nil && !test.wantUnmarshalErr {
t.Errorf("For %T unmarshal %q, want %v, got error: %v", test.value, test.str, test.value, err)
} else if err == nil && test.wantUnmarshalErr {
t.Errorf("For %T unmarshal %q, want error, got %v", test.value, test.str, resultValue)
} else if err == nil && !test.value.Equal(resultValue) {
t.Errorf("For %T unmarshal %q, want %v, got %v", test.value, test.str, test.value, resultValue)
}
}
}

View File

@ -0,0 +1,202 @@
package ssdp
import (
"fmt"
"log"
"net/http"
"net/url"
"regexp"
"strconv"
"sync"
"time"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/httpu"
)
const (
maxExpiryTimeSeconds = 24 * 60 * 60
)
var (
maxAgeRx = regexp.MustCompile("max-age=([0-9]+)")
)
type Entry struct {
// The address that the entry data was actually received from.
RemoteAddr string
// Unique Service Name. Identifies a unique instance of a device or service.
USN string
// Notfication Type. The type of device or service being announced.
NT string
// Server's self-identifying string.
Server string
Host string
// Location of the UPnP root device description.
Location *url.URL
// Despite BOOTID,CONFIGID being required fields, apparently they are not
// always set by devices. Set to -1 if not present.
BootID int32
ConfigID int32
SearchPort uint16
// When the last update was received for this entry identified by this USN.
LastUpdate time.Time
// When the last update's cached values are advised to expire.
CacheExpiry time.Time
}
func newEntryFromRequest(r *http.Request) (*Entry, error) {
now := time.Now()
expiryDuration, err := parseCacheControlMaxAge(r.Header.Get("CACHE-CONTROL"))
if err != nil {
return nil, fmt.Errorf("ssdp: error parsing CACHE-CONTROL max age: %v", err)
}
loc, err := url.Parse(r.Header.Get("LOCATION"))
if err != nil {
return nil, fmt.Errorf("ssdp: error parsing entry Location URL: %v", err)
}
bootID, err := parseUpnpIntHeader(r.Header, "BOOTID.UPNP.ORG", -1)
if err != nil {
return nil, err
}
configID, err := parseUpnpIntHeader(r.Header, "CONFIGID.UPNP.ORG", -1)
if err != nil {
return nil, err
}
searchPort, err := parseUpnpIntHeader(r.Header, "SEARCHPORT.UPNP.ORG", ssdpSearchPort)
if err != nil {
return nil, err
}
if searchPort < 1 || searchPort > 65535 {
return nil, fmt.Errorf("ssdp: search port %d is out of range", searchPort)
}
return &Entry{
RemoteAddr: r.RemoteAddr,
USN: r.Header.Get("USN"),
NT: r.Header.Get("NT"),
Server: r.Header.Get("SERVER"),
Host: r.Header.Get("HOST"),
Location: loc,
BootID: bootID,
ConfigID: configID,
SearchPort: uint16(searchPort),
LastUpdate: now,
CacheExpiry: now.Add(expiryDuration),
}, nil
}
func parseCacheControlMaxAge(cc string) (time.Duration, error) {
matches := maxAgeRx.FindStringSubmatch(cc)
if len(matches) != 2 {
return 0, fmt.Errorf("did not find exactly one max-age in cache control header: %q", cc)
}
expirySeconds, err := strconv.ParseInt(matches[1], 10, 16)
if err != nil {
return 0, err
}
if expirySeconds < 1 || expirySeconds > maxExpiryTimeSeconds {
return 0, fmt.Errorf("rejecting bad expiry time of %d seconds", expirySeconds)
}
return time.Duration(expirySeconds) * time.Second, nil
}
// parseUpnpIntHeader is intended to parse the
// {BOOT,CONFIGID,SEARCHPORT}.UPNP.ORG header fields. It returns the def if
// the head is empty or missing.
func parseUpnpIntHeader(headers http.Header, headerName string, def int32) (int32, error) {
s := headers.Get(headerName)
if s == "" {
return def, nil
}
v, err := strconv.ParseInt(s, 10, 32)
if err != nil {
return 0, fmt.Errorf("ssdp: could not parse header %s: %v", headerName, err)
}
return int32(v), nil
}
var _ httpu.Handler = new(Registry)
// Registry maintains knowledge of discovered devices and services.
type Registry struct {
lock sync.Mutex
byUSN map[string]*Entry
}
func NewRegistry() *Registry {
return &Registry{
byUSN: make(map[string]*Entry),
}
}
// ServeMessage implements httpu.Handler, and uses SSDP NOTIFY requests to
// maintain the registry of devices and services.
func (reg *Registry) ServeMessage(r *http.Request) {
if r.Method != methodNotify {
return
}
nts := r.Header.Get("nts")
var err error
switch nts {
case ntsAlive:
err = reg.handleNTSAlive(r)
case ntsUpdate:
err = reg.handleNTSUpdate(r)
case ntsByebye:
err = reg.handleNTSByebye(r)
default:
err = fmt.Errorf("unknown NTS value: %q", nts)
}
log.Printf("In %s request from %s: %v", nts, r.RemoteAddr, err)
}
func (reg *Registry) handleNTSAlive(r *http.Request) error {
entry, err := newEntryFromRequest(r)
if err != nil {
return err
}
reg.lock.Lock()
defer reg.lock.Unlock()
reg.byUSN[entry.USN] = entry
return nil
}
func (reg *Registry) handleNTSUpdate(r *http.Request) error {
entry, err := newEntryFromRequest(r)
if err != nil {
return err
}
nextBootID, err := parseUpnpIntHeader(r.Header, "NEXTBOOTID.UPNP.ORG", -1)
if err != nil {
return err
}
entry.BootID = nextBootID
reg.lock.Lock()
defer reg.lock.Unlock()
reg.byUSN[entry.USN] = entry
return nil
}
func (reg *Registry) handleNTSByebye(r *http.Request) error {
reg.lock.Lock()
defer reg.lock.Unlock()
delete(reg.byUSN, r.Header.Get("USN"))
return nil
}

View File

@ -0,0 +1,83 @@
package ssdp
import (
"errors"
"log"
"net/http"
"net/url"
"strconv"
"time"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/huin/goupnp/httpu"
)
const (
ssdpDiscover = `"ssdp:discover"`
ntsAlive = `ssdp:alive`
ntsByebye = `ssdp:byebye`
ntsUpdate = `ssdp:update`
ssdpUDP4Addr = "239.255.255.250:1900"
ssdpSearchPort = 1900
methodSearch = "M-SEARCH"
methodNotify = "NOTIFY"
)
// SSDPRawSearch performs a fairly raw SSDP search request, and returns the
// unique response(s) that it receives. Each response has the requested
// searchTarget, a USN, and a valid location. maxWaitSeconds states how long to
// wait for responses in seconds, and must be a minimum of 1 (the
// implementation waits an additional 100ms for responses to arrive), 2 is a
// reasonable value for this. numSends is the number of requests to send - 3 is
// a reasonable value for this.
func SSDPRawSearch(httpu *httpu.HTTPUClient, searchTarget string, maxWaitSeconds int, numSends int) ([]*http.Response, error) {
if maxWaitSeconds < 1 {
return nil, errors.New("ssdp: maxWaitSeconds must be >= 1")
}
seenUsns := make(map[string]bool)
var responses []*http.Response
req := http.Request{
Method: methodSearch,
// TODO: Support both IPv4 and IPv6.
Host: ssdpUDP4Addr,
URL: &url.URL{Opaque: "*"},
Header: http.Header{
// Putting headers in here avoids them being title-cased.
// (The UPnP discovery protocol uses case-sensitive headers)
"HOST": []string{ssdpUDP4Addr},
"MX": []string{strconv.FormatInt(int64(maxWaitSeconds), 10)},
"MAN": []string{ssdpDiscover},
"ST": []string{searchTarget},
},
}
allResponses, err := httpu.Do(&req, time.Duration(maxWaitSeconds)*time.Second+100*time.Millisecond, numSends)
if err != nil {
return nil, err
}
for _, response := range allResponses {
if response.StatusCode != 200 {
log.Printf("ssdp: got response status code %q in search response", response.Status)
continue
}
if st := response.Header.Get("ST"); st != searchTarget {
log.Printf("ssdp: got unexpected search target result %q", st)
continue
}
location, err := response.Location()
if err != nil {
log.Printf("ssdp: no usable location in search response (discarding): %v", err)
continue
}
usn := response.Header.Get("USN")
if usn == "" {
log.Printf("ssdp: empty/missing USN in search response (using location instead): %v", err)
usn = location.String()
}
if _, alreadySeen := seenUsns[usn]; !alreadySeen {
seenUsns[usn] = true
responses = append(responses, response)
}
}
return responses, nil
}

View File

@ -0,0 +1,13 @@
Copyright 2013 John Howard Palevich
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,48 @@
go-nat-pmp
==========
A Go language client for the NAT-PMP internet protocol for port mapping and discovering the external
IP address of a firewall.
NAT-PMP is supported by Apple brand routers and open source routers like Tomato and DD-WRT.
See http://tools.ietf.org/html/draft-cheshire-nat-pmp-03
Get the package
---------------
go get -u github.com/jackpal/go-nat-pmp
Usage
-----
import natpmp "github.com/jackpal/go-nat-pmp"
client := natpmp.NewClient(gatewayIP)
response, err := client.GetExternalAddress()
if err != nil {
return
}
print("External IP address:", response.ExternalIPAddress)
Notes
-----
There doesn't seem to be an easy way to programmatically determine the address of the default gateway.
(Linux and OSX have a "routes" kernel API that can be examined to get this information, but there is
no Go package for getting this information.)
Clients
-------
This library is used in the Taipei Torrent BitTorrent client http://github.com/jackpal/Taipei-Torrent
Complete documentation
----------------------
http://godoc.org/github.com/jackpal/go-nat-pmp
License
-------
This project is licensed under the Apache License 2.0.

View File

@ -0,0 +1,184 @@
package natpmp
import (
"fmt"
"log"
"net"
"time"
)
// Implement the NAT-PMP protocol, typically supported by Apple routers and open source
// routers such as DD-WRT and Tomato.
//
// See http://tools.ietf.org/html/draft-cheshire-nat-pmp-03
//
// Usage:
//
// client := natpmp.NewClient(gatewayIP)
// response, err := client.GetExternalAddress()
const nAT_PMP_PORT = 5351
const nAT_TRIES = 9
const nAT_INITIAL_MS = 250
// The recommended mapping lifetime for AddPortMapping
const RECOMMENDED_MAPPING_LIFETIME_SECONDS = 3600
// Client is a NAT-PMP protocol client.
type Client struct {
gateway net.IP
}
// Create a NAT-PMP client for the NAT-PMP server at the gateway.
func NewClient(gateway net.IP) (nat *Client) {
return &Client{gateway}
}
// Results of the NAT-PMP GetExternalAddress operation
type GetExternalAddressResult struct {
SecondsSinceStartOfEpoc uint32
ExternalIPAddress [4]byte
}
// Get the external address of the router.
func (n *Client) GetExternalAddress() (result *GetExternalAddressResult, err error) {
msg := make([]byte, 2)
msg[0] = 0 // Version 0
msg[1] = 0 // OP Code 0
response, err := n.rpc(msg, 12)
if err != nil {
return
}
result = &GetExternalAddressResult{}
result.SecondsSinceStartOfEpoc = readNetworkOrderUint32(response[4:8])
copy(result.ExternalIPAddress[:], response[8:12])
return
}
// Results of the NAT-PMP AddPortMapping operation
type AddPortMappingResult struct {
SecondsSinceStartOfEpoc uint32
InternalPort uint16
MappedExternalPort uint16
PortMappingLifetimeInSeconds uint32
}
// Add (or delete) a port mapping. To delete a mapping, set the requestedExternalPort and lifetime to 0
func (n *Client) AddPortMapping(protocol string, internalPort, requestedExternalPort int, lifetime int) (result *AddPortMappingResult, err error) {
var opcode byte
if protocol == "udp" {
opcode = 1
} else if protocol == "tcp" {
opcode = 2
} else {
err = fmt.Errorf("unknown protocol %v", protocol)
return
}
msg := make([]byte, 12)
msg[0] = 0 // Version 0
msg[1] = opcode
writeNetworkOrderUint16(msg[4:6], uint16(internalPort))
writeNetworkOrderUint16(msg[6:8], uint16(requestedExternalPort))
writeNetworkOrderUint32(msg[8:12], uint32(lifetime))
response, err := n.rpc(msg, 16)
if err != nil {
return
}
result = &AddPortMappingResult{}
result.SecondsSinceStartOfEpoc = readNetworkOrderUint32(response[4:8])
result.InternalPort = readNetworkOrderUint16(response[8:10])
result.MappedExternalPort = readNetworkOrderUint16(response[10:12])
result.PortMappingLifetimeInSeconds = readNetworkOrderUint32(response[12:16])
return
}
func (n *Client) rpc(msg []byte, resultSize int) (result []byte, err error) {
var server net.UDPAddr
server.IP = n.gateway
server.Port = nAT_PMP_PORT
conn, err := net.DialUDP("udp", nil, &server)
if err != nil {
return
}
defer conn.Close()
result = make([]byte, resultSize)
needNewDeadline := true
var tries uint
for tries = 0; tries < nAT_TRIES; {
if needNewDeadline {
err = conn.SetDeadline(time.Now().Add((nAT_INITIAL_MS << tries) * time.Millisecond))
if err != nil {
return
}
needNewDeadline = false
}
_, err = conn.Write(msg)
if err != nil {
return
}
var bytesRead int
var remoteAddr *net.UDPAddr
bytesRead, remoteAddr, err = conn.ReadFromUDP(result)
if err != nil {
if err.(net.Error).Timeout() {
tries++
needNewDeadline = true
continue
}
return
}
if !remoteAddr.IP.Equal(n.gateway) {
log.Printf("Ignoring packet because IPs differ:", remoteAddr, n.gateway)
// Ignore this packet.
// Continue without increasing retransmission timeout or deadline.
continue
}
if bytesRead != resultSize {
err = fmt.Errorf("unexpected result size %d, expected %d", bytesRead, resultSize)
return
}
if result[0] != 0 {
err = fmt.Errorf("unknown protocol version %d", result[0])
return
}
expectedOp := msg[1] | 0x80
if result[1] != expectedOp {
err = fmt.Errorf("Unexpected opcode %d. Expected %d", result[1], expectedOp)
return
}
resultCode := readNetworkOrderUint16(result[2:4])
if resultCode != 0 {
err = fmt.Errorf("Non-zero result code %d", resultCode)
return
}
// If we got here the RPC is good.
return
}
err = fmt.Errorf("Timed out trying to contact gateway")
return
}
func writeNetworkOrderUint16(buf []byte, d uint16) {
buf[0] = byte(d >> 8)
buf[1] = byte(d)
}
func writeNetworkOrderUint32(buf []byte, d uint32) {
buf[0] = byte(d >> 24)
buf[1] = byte(d >> 16)
buf[2] = byte(d >> 8)
buf[3] = byte(d)
}
func readNetworkOrderUint16(buf []byte) uint16 {
return (uint16(buf[0]) << 8) | uint16(buf[1])
}
func readNetworkOrderUint32(buf []byte) uint32 {
return (uint32(buf[0]) << 24) | (uint32(buf[1]) << 16) | (uint32(buf[2]) << 8) | uint32(buf[3])
}

View File

@ -35,6 +35,10 @@ func newProcess(tf TeardownFunc) *process {
}
func (p *process) WaitFor(q Process) {
if q == nil {
panic("waiting for nil process")
}
p.Lock()
select {
@ -48,6 +52,10 @@ func (p *process) WaitFor(q Process) {
}
func (p *process) AddChildNoWait(child Process) {
if child == nil {
panic("adding nil child process")
}
p.Lock()
select {
@ -61,6 +69,10 @@ func (p *process) AddChildNoWait(child Process) {
}
func (p *process) AddChild(child Process) {
if child == nil {
panic("adding nil child process")
}
p.Lock()
select {

View File

@ -389,15 +389,13 @@ func loadPrivateKey(cfg *config.Identity, id peer.ID) (ic.PrivKey, error) {
}
func listenAddresses(cfg *config.Config) ([]ma.Multiaddr, error) {
var err error
listen := make([]ma.Multiaddr, len(cfg.Addresses.Swarm))
for i, addr := range cfg.Addresses.Swarm {
listen[i], err = ma.NewMultiaddr(addr)
var listen []ma.Multiaddr
for _, addr := range cfg.Addresses.Swarm {
maddr, err := ma.NewMultiaddr(addr)
if err != nil {
return nil, fmt.Errorf("Failure to parse config.Addresses.Swarm[%d]: %s", i, cfg.Addresses.Swarm)
return nil, fmt.Errorf("Failure to parse config.Addresses.Swarm: %s", cfg.Addresses.Swarm)
}
listen = append(listen, maddr)
}
return listen, nil
@ -413,7 +411,7 @@ func constructPeerHost(ctx context.Context, cfg *config.Config, id peer.ID, ps p
// make sure we error out if our config does not have addresses we can use
log.Debugf("Config.Addresses.Swarm:%s", listenAddrs)
filteredAddrs := addrutil.FilterUsableAddrs(listenAddrs)
log.Debugf("Config.Addresses.Swarm:%s (filtered)", listenAddrs)
log.Debugf("Config.Addresses.Swarm:%s (filtered)", filteredAddrs)
if len(filteredAddrs) < 1 {
return nil, debugerror.Errorf("addresses in config not usable: %s", listenAddrs)
}
@ -423,7 +421,7 @@ func constructPeerHost(ctx context.Context, cfg *config.Config, id peer.ID, ps p
return nil, debugerror.Wrap(err)
}
peerhost := p2pbhost.New(network)
peerhost := p2pbhost.New(network, p2pbhost.NATPortMap)
// explicitly set these as our listen addrs.
// (why not do it inside inet.NewNetwork? because this way we can
// listen on addresses without necessarily advertising those publicly.)
@ -431,7 +429,8 @@ func constructPeerHost(ctx context.Context, cfg *config.Config, id peer.ID, ps p
if err != nil {
return nil, debugerror.Wrap(err)
}
log.Info("Swarm listening at: %s", addrs)
log.Infof("Swarm listening at: %s", addrs)
ps.AddAddresses(id, addrs)
return peerhost, nil
}

View File

@ -1,10 +1,15 @@
package basichost
import (
"sync"
context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
goprocess "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/goprocess"
eventlog "github.com/jbenet/go-ipfs/thirdparty/eventlog"
inat "github.com/jbenet/go-ipfs/p2p/nat"
inet "github.com/jbenet/go-ipfs/p2p/net"
peer "github.com/jbenet/go-ipfs/p2p/peer"
protocol "github.com/jbenet/go-ipfs/p2p/protocol"
@ -14,20 +19,47 @@ import (
var log = eventlog.Logger("p2p/host/basic")
// Option is a type used to pass in options to the host.
type Option int
const (
// NATPortMap makes the host attempt to open port-mapping in NAT devices
// for all its listeners. Pass in this option in the constructor to
// asynchronously a) find a gateway, b) open port mappings, c) republish
// port mappings periodically. The NATed addresses are included in the
// Host's Addrs() list.
NATPortMap Option = iota
)
// BasicHost is the basic implementation of the host.Host interface. This
// particular host implementation:
// * uses a protocol muxer to mux per-protocol streams
// * uses an identity service to send + receive node information
// * uses a relay service to allow hosts to relay conns for each other
// * uses a nat service to establish NAT port mappings
type BasicHost struct {
network inet.Network
mux *protocol.Mux
ids *identify.IDService
relay *relay.RelayService
natmu sync.Mutex
nat *inat.NAT
proc goprocess.Process
}
// New constructs and sets up a new *BasicHost with given Network
func New(net inet.Network) *BasicHost {
func New(net inet.Network, opts ...Option) *BasicHost {
h := &BasicHost{
network: net,
mux: protocol.NewMux(),
}
h.proc = goprocess.WithTeardown(func() error {
return h.Network().Close()
})
// setup host services
h.ids = identify.NewIDService(h)
h.relay = relay.NewRelayService(h, h.Mux().HandleSync)
@ -35,9 +67,48 @@ func New(net inet.Network) *BasicHost {
net.SetConnHandler(h.newConnHandler)
net.SetStreamHandler(h.newStreamHandler)
for _, o := range opts {
switch o {
case NATPortMap:
h.setupNATPortMap()
}
}
return h
}
func (h *BasicHost) setupNATPortMap() {
// do this asynchronously to avoid blocking daemon startup
h.proc.Go(func(worker goprocess.Process) {
nat := inat.DiscoverNAT()
if nat == nil { // no nat, or failed to get it.
return
}
select {
case <-worker.Closing():
nat.Close()
return
default:
}
// wire up the nat to close when proc closes.
h.proc.AddChild(nat.Process())
h.natmu.Lock()
h.nat = nat
h.natmu.Unlock()
addrs := h.Network().ListenAddresses()
nat.PortMapAddrs(addrs)
mapAddrs := nat.ExternalAddrs()
if len(mapAddrs) > 0 {
log.Infof("NAT mapping addrs: %s", mapAddrs)
}
})
}
// newConnHandler is the remote-opened conn handler for inet.Network
func (h *BasicHost) newConnHandler(c inet.Conn) {
h.ids.IdentifyConn(c)
@ -143,7 +214,23 @@ func (h *BasicHost) dialPeer(ctx context.Context, p peer.ID) error {
return nil
}
func (h *BasicHost) Addrs() []ma.Multiaddr {
addrs, err := h.Network().InterfaceListenAddresses()
if err != nil {
log.Debug("error retrieving network interface addrs")
}
h.natmu.Lock()
nat := h.nat
h.natmu.Unlock()
if nat != nil {
addrs = append(addrs, nat.ExternalAddrs()...)
}
return addrs
}
// Close shuts down the Host's services (network, etc).
func (h *BasicHost) Close() error {
return h.Network().Close()
return h.proc.Close()
}

View File

@ -2,6 +2,7 @@ package host
import (
context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
inet "github.com/jbenet/go-ipfs/p2p/net"
peer "github.com/jbenet/go-ipfs/p2p/peer"
@ -23,6 +24,9 @@ type Host interface {
// Peerstore returns the Host's repository of Peer Addresses and Keys.
Peerstore() peer.Peerstore
// Returns the listen addresses of the Host
Addrs() []ma.Multiaddr
// Networks returns the Network interface of the Host
Network() inet.Network

410
p2p/nat/nat.go Normal file
View File

@ -0,0 +1,410 @@
package nat
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
manet "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr-net"
nat "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/fd/go-nat"
goprocess "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/goprocess"
periodic "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/goprocess/periodic"
eventlog "github.com/jbenet/go-ipfs/thirdparty/eventlog"
notifier "github.com/jbenet/go-ipfs/thirdparty/notifier"
)
var (
// ErrNoMapping signals no mapping exists for an address
ErrNoMapping = errors.New("mapping not established")
)
var log = eventlog.Logger("nat")
// MappingDuration is a default port mapping duration.
// Port mappings are renewed every (MappingDuration / 3)
const MappingDuration = time.Second * 60
// DiscoverNAT looks for a NAT device in the network and
// returns an object that can manage port mappings.
func DiscoverNAT() *NAT {
nat, err := nat.DiscoverGateway()
if err != nil {
log.Debug("DiscoverGateway error:", err)
return nil
}
addr, err := nat.GetDeviceAddress()
if err != nil {
log.Debug("DiscoverGateway address error:", err)
} else {
log.Debug("DiscoverGateway address:", addr)
}
return newNAT(nat)
}
// NAT is an object that manages address port mappings in
// NATs (Network Address Translators). It is a long-running
// service that will periodically renew port mappings,
// and keep an up-to-date list of all the external addresses.
type NAT struct {
nat nat.NAT
proc goprocess.Process // manages nat mappings lifecycle
mappingmu sync.RWMutex // guards mappings
mappings []*mapping
Notifier
}
func newNAT(realNAT nat.NAT) *NAT {
return &NAT{
nat: realNAT,
proc: goprocess.WithParent(goprocess.Background()),
}
}
// Close shuts down all port mappings. NAT can no longer be used.
func (nat *NAT) Close() error {
return nat.proc.Close()
}
// Process returns the nat's life-cycle manager, for making it listen
// to close signals.
func (nat *NAT) Process() goprocess.Process {
return nat.proc
}
// Notifier is an object that assists NAT in notifying listeners.
// It is implemented using github.com/jbenet/go-ipfs/thirdparty/notifier
type Notifier struct {
n notifier.Notifier
}
func (n *Notifier) notifyAll(notify func(n Notifiee)) {
n.n.NotifyAll(func(n notifier.Notifiee) {
notify(n.(Notifiee))
})
}
// Notify signs up notifiee to listen to NAT events.
func (n *Notifier) Notify(notifiee Notifiee) {
n.n.Notify(n)
}
// StopNotify stops signaling events to notifiee.
func (n *Notifier) StopNotify(notifiee Notifiee) {
n.n.StopNotify(notifiee)
}
// Notifiee is an interface objects must implement to listen to NAT events.
type Notifiee interface {
// Called every time a successful mapping happens
// Warning: the port mapping may have changed. If that is the
// case, both MappingSuccess and MappingChanged are called.
MappingSuccess(nat *NAT, m Mapping)
// Called when mapping a port succeeds, but the mapping is
// with a different port than an earlier success.
MappingChanged(nat *NAT, m Mapping, oldport int, newport int)
// Called when a port mapping fails. NAT will continue attempting after
// the next period. To stop trying, use: mapping.Close(). After this failure,
// mapping.ExternalPort() will be zero, and nat.ExternalAddrs() will not
// return the address for this mapping. With luck, the next attempt will
// succeed, without the client needing to do anything.
MappingFailed(nat *NAT, m Mapping, oldport int, err error)
}
// Mapping represents a port mapping in a NAT.
type Mapping interface {
// NAT returns the NAT object this Mapping belongs to.
NAT() *NAT
// Protocol returns the protocol of this port mapping. This is either
// "tcp" or "udp" as no other protocols are likely to be NAT-supported.
Protocol() string
// InternalPort returns the internal device port. Mapping will continue to
// try to map InternalPort() to an external facing port.
InternalPort() int
// ExternalPort returns the external facing port. If the mapping is not
// established, port will be 0
ExternalPort() int
// InternalAddr returns the internal address.
InternalAddr() ma.Multiaddr
// ExternalAddr returns the external facing address. If the mapping is not
// established, addr will be nil, and and ErrNoMapping will be returned.
ExternalAddr() (addr ma.Multiaddr, err error)
}
// keeps republishing
type mapping struct {
sync.Mutex // guards all fields
nat *NAT
proto string
intport int
extport int
intaddr ma.Multiaddr
proc goprocess.Process
}
func (m *mapping) NAT() *NAT {
m.Lock()
defer m.Unlock()
return m.nat
}
func (m *mapping) Protocol() string {
m.Lock()
defer m.Unlock()
return m.proto
}
func (m *mapping) InternalPort() int {
m.Lock()
defer m.Unlock()
return m.intport
}
func (m *mapping) ExternalPort() int {
m.Lock()
defer m.Unlock()
return m.extport
}
func (m *mapping) setExternalPort(p int) {
m.Lock()
defer m.Unlock()
m.extport = p
}
func (m *mapping) InternalAddr() ma.Multiaddr {
m.Lock()
defer m.Unlock()
return m.intaddr
}
func (m *mapping) ExternalAddr() (ma.Multiaddr, error) {
if m.ExternalPort() == 0 { // dont even try right now.
return nil, ErrNoMapping
}
ip, err := m.nat.nat.GetExternalAddress()
if err != nil {
return nil, err
}
ipmaddr, err := manet.FromIP(ip)
if err != nil {
return nil, fmt.Errorf("error parsing ip")
}
// call m.ExternalPort again, as mapping may have changed under our feet. (tocttou)
extport := m.ExternalPort()
if extport == 0 {
return nil, ErrNoMapping
}
tcp, err := ma.NewMultiaddr(fmt.Sprintf("/%s/%d", m.Protocol(), extport))
if err != nil {
return nil, err
}
maddr2 := ipmaddr.Encapsulate(tcp)
return maddr2, nil
}
func (m *mapping) Close() error {
return m.proc.Close()
}
// Mappings returns a slice of all NAT mappings
func (nat *NAT) Mappings() []Mapping {
nat.mappingmu.Lock()
maps2 := make([]Mapping, len(nat.mappings))
for i, m := range nat.mappings {
maps2[i] = m
}
nat.mappingmu.Unlock()
return maps2
}
func (nat *NAT) addMapping(m *mapping) {
// make mapping automatically close when nat is closed.
nat.proc.AddChild(m.proc)
nat.mappingmu.Lock()
nat.mappings = append(nat.mappings, m)
nat.mappingmu.Unlock()
}
// NewMapping attemps to construct a mapping on protocol and internal port
// It will also periodically renew the mapping until the returned Mapping
// -- or its parent NAT -- is Closed.
//
// May not succeed, and mappings may change over time;
// NAT devices may not respect our port requests, and even lie.
// Clients should not store the mapped results, but rather always
// poll our object for the latest mappings.
func (nat *NAT) NewMapping(maddr ma.Multiaddr) (Mapping, error) {
if nat == nil {
return nil, fmt.Errorf("no nat available")
}
network, addr, err := manet.DialArgs(maddr)
if err != nil {
return nil, fmt.Errorf("DialArgs failed on addr:", maddr.String())
}
switch network {
case "tcp", "tcp4", "tcp6":
network = "tcp"
case "udp", "udp4", "udp6":
network = "udp"
default:
return nil, fmt.Errorf("transport not supported by NAT: %s", network)
}
intports := strings.Split(addr, ":")[1]
intport, err := strconv.Atoi(intports)
if err != nil {
return nil, err
}
m := &mapping{
nat: nat,
proto: network,
intport: intport,
intaddr: maddr,
}
m.proc = periodic.Every(MappingDuration/3, func(worker goprocess.Process) {
nat.establishMapping(m)
})
nat.addMapping(m)
// do it once synchronously, so first mapping is done right away, and before exiting,
// allowing users -- in the optimistic case -- to use results right after.
nat.establishMapping(m)
return m, nil
}
func (nat *NAT) establishMapping(m *mapping) {
oldport := m.ExternalPort()
log.Debugf("Attempting port map: %s/%d", m.Protocol(), m.InternalPort())
newport, err := nat.nat.AddPortMapping(m.Protocol(), m.InternalPort(), "http", MappingDuration)
failure := func() {
m.setExternalPort(0) // clear mapping
// TODO: log.Event
log.Infof("failed to establish port mapping: %s", err)
nat.Notifier.notifyAll(func(n Notifiee) {
n.MappingFailed(nat, m, oldport, err)
})
// we do not close if the mapping failed,
// because it may work again next time.
}
if err != nil || newport == 0 {
failure()
return
}
m.setExternalPort(newport)
ext, err := m.ExternalAddr()
if err != nil {
log.Debugf("NAT Mapping addr error: %s %s", m.InternalAddr(), err)
failure()
return
}
log.Debugf("NAT Mapping: %s --> %s", m.InternalAddr(), ext)
if oldport != 0 && newport != oldport {
log.Infof("failed to renew same port mapping: ch %d -> %d", oldport, newport)
nat.Notifier.notifyAll(func(n Notifiee) {
n.MappingChanged(nat, m, oldport, newport)
})
}
nat.Notifier.notifyAll(func(n Notifiee) {
n.MappingSuccess(nat, m)
})
}
// PortMapAddrs attempts to open (and continue to keep open)
// port mappings for given addrs. This function blocks until
// all addresses have been tried. This allows clients to
// retrieve results immediately after:
//
// nat.PortMapAddrs(addrs)
// mapped := nat.ExternalAddrs()
//
// Some may not succeed, and mappings may change over time;
// NAT devices may not respect our port requests, and even lie.
// Clients should not store the mapped results, but rather always
// poll our object for the latest mappings.
func (nat *NAT) PortMapAddrs(addrs []ma.Multiaddr) {
// spin off addr mappings independently.
var wg sync.WaitGroup
for _, addr := range addrs {
// do all of them concurrently
wg.Add(1)
go func() {
defer wg.Done()
nat.NewMapping(addr)
}()
}
wg.Wait()
}
// MappedAddrs returns address mappings NAT believes have been
// successfully established. Unsuccessful mappings are nil. This is:
//
// map[internalAddr]externalAddr
//
// This set of mappings _may not_ be correct, as NAT devices are finicky.
// Consider this with _best effort_ semantics.
func (nat *NAT) MappedAddrs() map[ma.Multiaddr]ma.Multiaddr {
mappings := nat.Mappings()
addrmap := make(map[ma.Multiaddr]ma.Multiaddr, len(mappings))
for _, m := range mappings {
i := m.InternalAddr()
e, err := m.ExternalAddr()
if err != nil {
addrmap[i] = nil
} else {
addrmap[i] = e
}
}
return addrmap
}
// ExternalAddrs returns a list of addresses that NAT believes have
// been successfully established. Unsuccessful mappings are omitted,
// so nat.ExternalAddrs() may return less addresses than nat.InternalAddrs().
// To see which addresses are mapped, use nat.MappedAddrs().
//
// This set of mappings _may not_ be correct, as NAT devices are finicky.
// Consider this with _best effort_ semantics.
func (nat *NAT) ExternalAddrs() []ma.Multiaddr {
mappings := nat.Mappings()
addrs := make([]ma.Multiaddr, 0, len(mappings))
for _, m := range mappings {
a, err := m.ExternalAddr()
if err != nil {
continue // this mapping not currently successful.
}
addrs = append(addrs, a)
}
return addrs
}

View File

@ -152,7 +152,7 @@ func reuseErrShouldRetry(err error) bool {
// if it's a network timeout error, it's a legitimate failure.
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return true
return false
}
errno, ok := err.(syscall.Errno)

View File

@ -67,11 +67,6 @@ func (mn *mocknet) AddPeer(k ic.PrivKey, a ma.Multiaddr) (host.Host, error) {
}
h := bhost.New(n)
// make sure to add listening address!
// this makes debugging things simpler as remembering to register
// an address may cause unexpected failure.
n.Peerstore().AddAddress(n.LocalPeer(), a)
log.Debugf("mocknet added listen addr for peer: %s -- %s", n.LocalPeer(), a)
mn.cg.AddChildGroup(n.cg)

View File

@ -141,17 +141,13 @@ func (ids *IDService) populateMessage(mes *pb.Identify, c inet.Conn) {
// "public" address, at least in relation to us.
mes.ObservedAddr = c.RemoteMultiaddr().Bytes()
// set listen addrs
laddrs, err := ids.Host.Network().InterfaceListenAddresses()
if err != nil {
log.Error(err)
} else {
mes.ListenAddrs = make([][]byte, len(laddrs))
for i, addr := range laddrs {
mes.ListenAddrs[i] = addr.Bytes()
}
log.Debugf("%s sent listen addrs to %s: %s", c.LocalPeer(), c.RemotePeer(), laddrs)
// set listen addrs, get our latest addrs from Host.
laddrs := ids.Host.Addrs()
mes.ListenAddrs = make([][]byte, len(laddrs))
for i, addr := range laddrs {
mes.ListenAddrs[i] = addr.Bytes()
}
log.Debugf("%s sent listen addrs to %s: %s", c.LocalPeer(), c.RemotePeer(), laddrs)
// set protocol versions
s := IpfsVersion.String()

115
thirdparty/notifier/notifier.go vendored Normal file
View File

@ -0,0 +1,115 @@
// Package notifier provides a simple notification dispatcher
// meant to be embedded in larger structres who wish to allow
// clients to sign up for event notifications.
package notifier
import (
"sync"
)
// Notifiee is a generic interface. Clients implement
// their own Notifiee interfaces to ensure type-safety
// of notifications:
//
// type RocketNotifiee interface{
// Countdown(r Rocket, countdown time.Duration)
// LiftedOff(Rocket)
// ReachedOrbit(Rocket)
// Detached(Rocket, Capsule)
// Landed(Rocket)
// }
//
type Notifiee interface{}
// Notifier is a notification dispatcher. It's meant
// to be composed, and its zero-value is ready to be used.
//
// type Rocket struct {
// notifier notifier.Notifier
// }
//
type Notifier struct {
mu sync.RWMutex // guards notifiees
nots map[Notifiee]struct{}
}
// Notify signs up Notifiee e for notifications. This function
// is meant to be called behind your own type-safe function(s):
//
// // generic function for pattern-following
// func (r *Rocket) Notify(n Notifiee) {
// r.notifier.Notify(n)
// }
//
// // or as part of other functions
// func (r *Rocket) Onboard(a Astronaut) {
// r.astronauts = append(r.austronauts, a)
// r.notifier.Notify(a)
// }
//
func (n *Notifier) Notify(e Notifiee) {
n.mu.Lock()
if n.nots == nil { // so that zero-value is ready to be used.
n.nots = make(map[Notifiee]struct{})
}
n.nots[e] = struct{}{}
n.mu.Unlock()
}
// StopNotifying stops notifying Notifiee e. This function
// is meant to be called behind your own type-safe function(s):
//
// // generic function for pattern-following
// func (r *Rocket) StopNotify(n Notifiee) {
// r.notifier.StopNotify(n)
// }
//
// // or as part of other functions
// func (r *Rocket) Detach(c Capsule) {
// r.notifier.StopNotify(c)
// r.capsule = nil
// }
//
func (n *Notifier) StopNotify(e Notifiee) {
n.mu.Lock()
if n.nots != nil { // so that zero-value is ready to be used.
delete(n.nots, e)
}
n.mu.Unlock()
}
// NotifyAll messages the notifier's notifiees with a given notification.
// This is done by calling the given function with each notifiee. It is
// meant to be called with your own type-safe notification functions:
//
// func (r *Rocket) Launch() {
// r.notifyAll(func(n Notifiee) {
// n.Launched(r)
// })
// }
//
// // make it private so only you can use it. This function is necessary
// // to make sure you only up-cast in one place. You control who you added
// // to be a notifiee. If Go adds generics, maybe we can get rid of this
// // method but for now it is like wrapping a type-less container with
// // a type safe interface.
// func (r *Rocket) notifyAll(notify func(Notifiee)) {
// r.notifier.NotifyAll(func(n notifier.Notifiee) {
// notify(n.(Notifiee))
// })
// }
//
// Note well: each notification is launched in its own goroutine, so they
// can be processed concurrently, and so that whatever the notification does
// it _never_ blocks out the client. This is so that consumers _cannot_ add
// hooks into your object that block you accidentally.
func (n *Notifier) NotifyAll(notify func(Notifiee)) {
n.mu.Lock()
if n.nots != nil { // so that zero-value is ready to be used.
for notifiee := range n.nots {
go notify(notifiee)
// TODO find a good way to rate limit this without blocking notifier.
}
}
n.mu.Unlock()
}

207
thirdparty/notifier/notifier_test.go vendored Normal file
View File

@ -0,0 +1,207 @@
package notifier
import (
"fmt"
"sync"
"testing"
)
// test data structures
type Router struct {
queue chan Packet
notifier Notifier
}
type Packet struct{}
type RouterNotifiee interface {
Enqueued(*Router, Packet)
Forwarded(*Router, Packet)
Dropped(*Router, Packet)
}
func (r *Router) Notify(n RouterNotifiee) {
r.notifier.Notify(n)
}
func (r *Router) StopNotify(n RouterNotifiee) {
r.notifier.StopNotify(n)
}
func (r *Router) notifyAll(notify func(n RouterNotifiee)) {
r.notifier.NotifyAll(func(n Notifiee) {
notify(n.(RouterNotifiee))
})
}
func (r *Router) Receive(p Packet) {
select {
case r.queue <- p: // enqueued
r.notifyAll(func(n RouterNotifiee) {
n.Enqueued(r, p)
})
default: // drop
r.notifyAll(func(n RouterNotifiee) {
n.Dropped(r, p)
})
}
}
func (r *Router) Forward() {
p := <-r.queue
r.notifyAll(func(n RouterNotifiee) {
n.Forwarded(r, p)
})
}
type Metrics struct {
enqueued int
forwarded int
dropped int
received chan struct{}
sync.Mutex
}
func (m *Metrics) Enqueued(*Router, Packet) {
m.Lock()
m.enqueued++
m.Unlock()
if m.received != nil {
m.received <- struct{}{}
}
}
func (m *Metrics) Forwarded(*Router, Packet) {
m.Lock()
m.forwarded++
m.Unlock()
if m.received != nil {
m.received <- struct{}{}
}
}
func (m *Metrics) Dropped(*Router, Packet) {
m.Lock()
m.dropped++
m.Unlock()
if m.received != nil {
m.received <- struct{}{}
}
}
func (m *Metrics) String() string {
m.Lock()
defer m.Unlock()
return fmt.Sprintf("%d enqueued, %d forwarded, %d in queue, %d dropped",
m.enqueued, m.forwarded, m.enqueued-m.forwarded, m.dropped)
}
func TestNotifies(t *testing.T) {
m := Metrics{received: make(chan struct{})}
r := Router{queue: make(chan Packet, 10)}
r.Notify(&m)
for i := 0; i < 10; i++ {
r.Receive(Packet{})
<-m.received
if m.enqueued != (1 + i) {
t.Error("not notifying correctly", m.enqueued, 1+i)
}
}
for i := 0; i < 10; i++ {
r.Receive(Packet{})
<-m.received
if m.enqueued != 10 {
t.Error("not notifying correctly", m.enqueued, 10)
}
if m.dropped != (1 + i) {
t.Error("not notifying correctly", m.dropped, 1+i)
}
}
}
func TestStopsNotifying(t *testing.T) {
m := Metrics{received: make(chan struct{})}
r := Router{queue: make(chan Packet, 10)}
r.Notify(&m)
for i := 0; i < 5; i++ {
r.Receive(Packet{})
<-m.received
if m.enqueued != (1 + i) {
t.Error("not notifying correctly")
}
}
r.StopNotify(&m)
for i := 0; i < 5; i++ {
r.Receive(Packet{})
select {
case <-m.received:
t.Error("did not stop notifying")
default:
}
if m.enqueued != 5 {
t.Error("did not stop notifying")
}
}
}
func TestThreadsafe(t *testing.T) {
N := 1000
r := Router{queue: make(chan Packet, 10)}
m1 := Metrics{received: make(chan struct{})}
m2 := Metrics{received: make(chan struct{})}
m3 := Metrics{received: make(chan struct{})}
r.Notify(&m1)
r.Notify(&m2)
r.Notify(&m3)
var n int
var wg sync.WaitGroup
for i := 0; i < N; i++ {
n++
wg.Add(1)
go func() {
defer wg.Done()
r.Receive(Packet{})
}()
if i%3 == 0 {
n++
wg.Add(1)
go func() {
defer wg.Done()
r.Forward()
}()
}
}
// drain queues
for i := 0; i < (n * 3); i++ {
select {
case <-m1.received:
case <-m2.received:
case <-m3.received:
}
}
wg.Wait()
// counts should be correct and all agree. and this should
// run fine under `go test -race -cpu=5`
t.Log("m1", m1.String())
t.Log("m2", m2.String())
t.Log("m3", m3.String())
if m1.String() != m2.String() || m2.String() != m3.String() {
t.Error("counts disagree")
}
}