Compare commits
No commits in common. "master" and "patch-1" have entirely different histories.
|
@ -2,26 +2,11 @@
|
||||||
"ImportPath": "github.com/s-rah/go-ricochet",
|
"ImportPath": "github.com/s-rah/go-ricochet",
|
||||||
"GoVersion": "go1.7",
|
"GoVersion": "go1.7",
|
||||||
"GodepVersion": "v79",
|
"GodepVersion": "v79",
|
||||||
"Packages": [
|
|
||||||
"./..."
|
|
||||||
],
|
|
||||||
"Deps": [
|
"Deps": [
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/golang/protobuf/proto",
|
"ImportPath": "github.com/golang/protobuf/proto",
|
||||||
"Rev": "8ee79997227bf9b34611aee7946ae64735e6fd93"
|
"Rev": "8ee79997227bf9b34611aee7946ae64735e6fd93"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"ImportPath": "github.com/yawning/bulb",
|
|
||||||
"Rev": "85d80d893c3d4a478b8c0abbc43f0ea13e1ce4f9"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/yawning/bulb/utils",
|
|
||||||
"Rev": "85d80d893c3d4a478b8c0abbc43f0ea13e1ce4f9"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ImportPath": "github.com/yawning/bulb/utils/pkcs1",
|
|
||||||
"Rev": "85d80d893c3d4a478b8c0abbc43f0ea13e1ce4f9"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/net/proxy",
|
"ImportPath": "golang.org/x/net/proxy",
|
||||||
"Rev": "60c41d1de8da134c05b7b40154a9a82bf5b7edb9"
|
"Rev": "60c41d1de8da134c05b7b40154a9a82bf5b7edb9"
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AcceptAllContactManager implements the contact manager interface an presumes
|
|
||||||
// all connections are allowed.
|
|
||||||
type AcceptAllContactManager struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupContact returns that a contact is known and allowed to communicate for all cases.
|
|
||||||
func (aacm *AcceptAllContactManager) LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
|
|
||||||
return true, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (aacm *AcceptAllContactManager) GetContactDetails() (string, string) {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (aacm *AcceptAllContactManager) ContactRequest(name string, message string) string {
|
|
||||||
return "Accepted"
|
|
||||||
}
|
|
|
@ -1,145 +1,36 @@
|
||||||
package application
|
package application
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"errors"
|
||||||
"github.com/s-rah/go-ricochet"
|
|
||||||
"github.com/s-rah/go-ricochet/channels"
|
"github.com/s-rah/go-ricochet/channels"
|
||||||
"github.com/s-rah/go-ricochet/connection"
|
"github.com/s-rah/go-ricochet/connection"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RicochetApplication bundles many useful constructs that are
|
// RicochetApplication bundles many useful constructs that are
|
||||||
// likely standard in a ricochet application
|
// likely standard in a ricochet application
|
||||||
type RicochetApplication struct {
|
type RicochetApplication struct {
|
||||||
contactManager ContactManagerInterface
|
connection *Connection
|
||||||
privateKey *rsa.PrivateKey
|
|
||||||
chatMessageHandler func(*RicochetApplicationInstance, uint32, time.Time, string)
|
|
||||||
chatMessageAckHandler func(*RicochetApplicationInstance, uint32)
|
|
||||||
l net.Listener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RicochetApplicationInstance struct {
|
// NewRicochetApplication ...
|
||||||
connection.AutoConnectionHandler
|
func NewRicochetApplication(connection *Connection) *RicochetApplication {
|
||||||
connection *connection.Connection
|
ra := new(RicochetApplication)
|
||||||
RemoteHostname string
|
ra.connection = connection
|
||||||
ChatMessageHandler func(*RicochetApplicationInstance, uint32, time.Time, string)
|
return ra
|
||||||
ChatMessageAckHandler func(*RicochetApplicationInstance, uint32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rai *RicochetApplicationInstance) GetContactDetails() (string, string) {
|
// SendMessage ...
|
||||||
return "EchoBot", "I LIVE 😈😈!!!!"
|
func (ra *RicochetApplication) SendChatMessage(message []string) error {
|
||||||
}
|
return ra.connection.Do(func() error {
|
||||||
|
channel := ra.connection.Channel("im.ricochet.chat", channels.Outbound)
|
||||||
func (rai *RicochetApplicationInstance) ContactRequest(name string, message string) string {
|
|
||||||
return "Accepted"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rai *RicochetApplicationInstance) ContactRequestRejected() {
|
|
||||||
}
|
|
||||||
func (rai *RicochetApplicationInstance) ContactRequestAccepted() {
|
|
||||||
}
|
|
||||||
func (rai *RicochetApplicationInstance) ContactRequestError() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rai *RicochetApplicationInstance) SendChatMessage(message string) {
|
|
||||||
|
|
||||||
// Technically this errors afte the second time but we can ignore it.
|
|
||||||
rai.connection.RequestOpenChannel("im.ricochet.chat", rai)
|
|
||||||
|
|
||||||
rai.connection.Do(func() error {
|
|
||||||
channel := rai.connection.Channel("im.ricochet.chat", channels.Outbound)
|
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
chatchannel, ok := (*channel.Handler).(*channels.ChatChannel)
|
chatchannel, ok := (*channel.Handler).(*channels.ChatChannel)
|
||||||
if ok {
|
if ok {
|
||||||
chatchannel.SendMessage(message)
|
chatchannel.SendMessage(message)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rai *RicochetApplicationInstance) ChatMessage(messageID uint32, when time.Time, message string) bool {
|
|
||||||
go rai.ChatMessageHandler(rai, messageID, when, message)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rai *RicochetApplicationInstance) ChatMessageAck(messageID uint32) {
|
|
||||||
rai.ChatMessageAckHandler(rai, messageID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ra *RicochetApplication) Init(pk *rsa.PrivateKey, cm ContactManagerInterface) {
|
|
||||||
ra.privateKey = pk
|
|
||||||
ra.contactManager = cm
|
|
||||||
ra.chatMessageHandler = func(*RicochetApplicationInstance, uint32, time.Time, string) {}
|
|
||||||
ra.chatMessageAckHandler = func(*RicochetApplicationInstance, uint32) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ra *RicochetApplication) OnChatMessage(call func(*RicochetApplicationInstance, uint32, time.Time, string)) {
|
|
||||||
ra.chatMessageHandler = call
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ra *RicochetApplication) OnChatMessageAck(call func(*RicochetApplicationInstance, uint32)) {
|
|
||||||
ra.chatMessageAckHandler = call
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ra *RicochetApplication) handleConnection(conn net.Conn) {
|
|
||||||
rc, err := goricochet.NegotiateVersionInbound(conn)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("There was an error")
|
|
||||||
conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ich := connection.HandleInboundConnection(rc)
|
|
||||||
|
|
||||||
err = ich.ProcessAuthAsServer(ra.privateKey, ra.contactManager.LookupContact)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("There was an error")
|
|
||||||
conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rai := new(RicochetApplicationInstance)
|
|
||||||
rai.Init(ra.privateKey, "")
|
|
||||||
rai.RemoteHostname = rc.RemoteHostname
|
|
||||||
rai.connection = rc
|
|
||||||
rai.ChatMessageHandler = ra.chatMessageHandler
|
|
||||||
rai.ChatMessageAckHandler = ra.chatMessageAckHandler
|
|
||||||
|
|
||||||
rai.RegisterChannelHandler("im.ricochet.contact.request", func() channels.Handler {
|
|
||||||
contact := new(channels.ContactRequestChannel)
|
|
||||||
contact.Handler = rai
|
|
||||||
return contact
|
|
||||||
})
|
|
||||||
rai.RegisterChannelHandler("im.ricochet.chat", func() channels.Handler {
|
|
||||||
chat := new(channels.ChatChannel)
|
|
||||||
chat.Handler = rai
|
|
||||||
return chat
|
|
||||||
})
|
|
||||||
rc.Process(rai)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ra *RicochetApplication) Shutdown () {
|
|
||||||
log.Printf("Closing")
|
|
||||||
ra.l.Close()
|
|
||||||
log.Printf("Closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ra *RicochetApplication) Run(l net.Listener) {
|
|
||||||
if ra.privateKey == nil || ra.contactManager == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ra.l = l
|
|
||||||
var err error
|
|
||||||
for err == nil {
|
|
||||||
conn, err := ra.l.Accept()
|
|
||||||
if err == nil {
|
|
||||||
go ra.handleConnection(conn)
|
|
||||||
} else {
|
|
||||||
log.Printf("Closing")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContactManagerInterface provides a mechanism for autonous applications
|
|
||||||
// to make decisions on what connections to accept or reject.
|
|
||||||
type ContactManagerInterface interface {
|
|
||||||
LookupContact(hostname string, publicKey rsa.PublicKey) (allowed, known bool)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/s-rah/go-ricochet/application"
|
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
echobot := new(application.RicochetApplication)
|
|
||||||
pk, err := utils.LoadPrivateKeyFromFile("./testing/private_key")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error reading private key file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", pk, 9878)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error setting up onion service: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
echobot.Init(pk, new(application.AcceptAllContactManager))
|
|
||||||
echobot.OnChatMessage(func(rai *application.RicochetApplicationInstance, id uint32, timestamp time.Time, message string) {
|
|
||||||
log.Printf("message from %v - %v", rai.RemoteHostname, message)
|
|
||||||
rai.SendChatMessage(message)
|
|
||||||
})
|
|
||||||
log.Printf("echobot listening on %s", l.Addr().String())
|
|
||||||
echobot.Run(l)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"github.com/yawning/bulb"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// "127.0.0.1:9051" "tcp4"
|
|
||||||
// "/var/run/tor/control" "unix"
|
|
||||||
func SetupOnion(torControlAddress string, torControlSocketType string, authentication string, pk *rsa.PrivateKey, onionport uint16) (net.Listener, error) {
|
|
||||||
c, err := bulb.Dial(torControlSocketType, torControlAddress)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Authenticate(authentication); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &bulb.NewOnionConfig{
|
|
||||||
DiscardPK: true,
|
|
||||||
PrivateKey: pk,
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.NewListener(cfg, onionport)
|
|
||||||
}
|
|
|
@ -12,7 +12,6 @@ const (
|
||||||
|
|
||||||
// AuthChannelResult captures the result of an authentication flow
|
// AuthChannelResult captures the result of an authentication flow
|
||||||
type AuthChannelResult struct {
|
type AuthChannelResult struct {
|
||||||
Hostname string
|
|
||||||
Accepted bool
|
Accepted bool
|
||||||
IsKnownContact bool
|
IsKnownContact bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
package channels
|
package channels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"github.com/s-rah/go-ricochet/utils"
|
||||||
"github.com/s-rah/go-ricochet/wire/contact"
|
"github.com/s-rah/go-ricochet/wire/contact"
|
||||||
"github.com/s-rah/go-ricochet/wire/control"
|
"github.com/s-rah/go-ricochet/wire/control"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defining Versions
|
|
||||||
const (
|
|
||||||
InvalidContactNameError = utils.Error("InvalidContactNameError")
|
|
||||||
InvalidContactMessageError = utils.Error("InvalidContactMessageError")
|
|
||||||
InvalidContactRequestError = utils.Error("InvalidContactRequestError")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContactRequestChannel implements the ChannelHandler interface for a channel of
|
// ContactRequestChannel implements the ChannelHandler interface for a channel of
|
||||||
// type "im.ricochet.contact.request". The channel may be inbound or outbound.
|
// type "im.ricochet.contact.request". The channel may be inbound or outbound.
|
||||||
// a ContactRequestChannelHandler implementation to handle chat events.
|
// a ContactRequestChannelHandler implementation to handle chat events.
|
||||||
|
@ -79,12 +73,12 @@ func (crc *ContactRequestChannel) OpenInbound(channel *Channel, oc *Protocol_Dat
|
||||||
|
|
||||||
if len(contactRequest.GetNickname()) > int(Protocol_Data_ContactRequest.Limits_NicknameMaxCharacters) {
|
if len(contactRequest.GetNickname()) > int(Protocol_Data_ContactRequest.Limits_NicknameMaxCharacters) {
|
||||||
// Violation of the Protocol
|
// Violation of the Protocol
|
||||||
return nil, InvalidContactNameError
|
return nil, errors.New("invalid nickname")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(contactRequest.GetMessageText()) > int(Protocol_Data_ContactRequest.Limits_MessageMaxCharacters) {
|
if len(contactRequest.GetMessageText()) > int(Protocol_Data_ContactRequest.Limits_MessageMaxCharacters) {
|
||||||
// Violation of the Protocol
|
// Violation of the Protocol
|
||||||
return nil, InvalidContactMessageError
|
return nil, errors.New("invalid message")
|
||||||
}
|
}
|
||||||
|
|
||||||
result := crc.Handler.ContactRequest(contactRequest.GetNickname(), contactRequest.GetMessageText())
|
result := crc.Handler.ContactRequest(contactRequest.GetNickname(), contactRequest.GetMessageText())
|
||||||
|
@ -92,7 +86,7 @@ func (crc *ContactRequestChannel) OpenInbound(channel *Channel, oc *Protocol_Dat
|
||||||
return messageBuilder.ReplyToContactRequestOnResponse(channel.ID, result), nil
|
return messageBuilder.ReplyToContactRequestOnResponse(channel.ID, result), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, InvalidContactRequestError
|
return nil, errors.New("could not parse contact request extension")
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenOutbound is the first method called for an outbound channel request.
|
// OpenOutbound is the first method called for an outbound channel request.
|
||||||
|
|
|
@ -7,15 +7,13 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"github.com/s-rah/go-ricochet/utils"
|
||||||
"github.com/s-rah/go-ricochet/wire/auth"
|
"github.com/s-rah/go-ricochet/wire/auth"
|
||||||
"github.com/s-rah/go-ricochet/wire/control"
|
"github.com/s-rah/go-ricochet/wire/control"
|
||||||
"io"
|
"io"
|
||||||
)
|
"log"
|
||||||
|
|
||||||
const (
|
|
||||||
InvalidClientCookieError = utils.Error("InvalidClientCookieError")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HiddenServiceAuthChannel wraps implementation of im.ricochet.auth.hidden-service"
|
// HiddenServiceAuthChannel wraps implementation of im.ricochet.auth.hidden-service"
|
||||||
|
@ -77,16 +75,11 @@ func (ah *HiddenServiceAuthChannel) Closed(err error) {
|
||||||
// returned, it will be sent as the ChannelResult message.
|
// returned, it will be sent as the ChannelResult message.
|
||||||
// Remote -> [Open Authentication Channel] -> Local
|
// Remote -> [Open Authentication Channel] -> Local
|
||||||
func (ah *HiddenServiceAuthChannel) OpenInbound(channel *Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) {
|
func (ah *HiddenServiceAuthChannel) OpenInbound(channel *Channel, oc *Protocol_Data_Control.OpenChannel) ([]byte, error) {
|
||||||
|
|
||||||
if ah.PrivateKey == nil {
|
|
||||||
return nil, utils.PrivateKeyNotSetError
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.channel = channel
|
ah.channel = channel
|
||||||
clientCookie, _ := proto.GetExtension(oc, Protocol_Data_AuthHiddenService.E_ClientCookie)
|
clientCookie, _ := proto.GetExtension(oc, Protocol_Data_AuthHiddenService.E_ClientCookie)
|
||||||
if len(clientCookie.([]byte)[:]) != 16 {
|
if len(clientCookie.([]byte)[:]) != 16 {
|
||||||
// reutrn without opening channel.
|
// reutrn without opening channel.
|
||||||
return nil, InvalidClientCookieError
|
return nil, errors.New("invalid client cookie")
|
||||||
}
|
}
|
||||||
ah.AddClientCookie(clientCookie.([]byte)[:])
|
ah.AddClientCookie(clientCookie.([]byte)[:])
|
||||||
messageBuilder := new(utils.MessageBuilder)
|
messageBuilder := new(utils.MessageBuilder)
|
||||||
|
@ -99,11 +92,6 @@ func (ah *HiddenServiceAuthChannel) OpenInbound(channel *Channel, oc *Protocol_D
|
||||||
// returned, it will be sent as the OpenChannel message.
|
// returned, it will be sent as the OpenChannel message.
|
||||||
// Local -> [Open Authentication Channel] -> Remote
|
// Local -> [Open Authentication Channel] -> Remote
|
||||||
func (ah *HiddenServiceAuthChannel) OpenOutbound(channel *Channel) ([]byte, error) {
|
func (ah *HiddenServiceAuthChannel) OpenOutbound(channel *Channel) ([]byte, error) {
|
||||||
|
|
||||||
if ah.PrivateKey == nil {
|
|
||||||
return nil, utils.PrivateKeyNotSetError
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.channel = channel
|
ah.channel = channel
|
||||||
messageBuilder := new(utils.MessageBuilder)
|
messageBuilder := new(utils.MessageBuilder)
|
||||||
return messageBuilder.OpenAuthenticationChannel(ah.channel.ID, ah.GenClientCookie()), nil
|
return messageBuilder.OpenAuthenticationChannel(ah.channel.ID, ah.GenClientCookie()), nil
|
||||||
|
@ -256,7 +244,7 @@ func (ah *HiddenServiceAuthChannel) GenChallenge(clientHostname string, serverHo
|
||||||
key := make([]byte, 32)
|
key := make([]byte, 32)
|
||||||
copy(key[0:16], ah.clientCookie[:])
|
copy(key[0:16], ah.clientCookie[:])
|
||||||
copy(key[16:], ah.serverCookie[:])
|
copy(key[16:], ah.serverCookie[:])
|
||||||
|
log.Printf("CHALLENGE: %s %s %v", clientHostname, serverHostname, key)
|
||||||
value := []byte(clientHostname + serverHostname)
|
value := []byte(clientHostname + serverHostname)
|
||||||
mac := hmac.New(sha256.New, key)
|
mac := hmac.New(sha256.New, key)
|
||||||
mac.Write(value)
|
mac.Write(value)
|
||||||
|
|
|
@ -71,10 +71,9 @@ func GetOpenAuthenticationChannelMessage() *Protocol_Data_Control.OpenChannel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticationOpenInbound(t *testing.T) {
|
func TestAuthenticationOpenInbound(t *testing.T) {
|
||||||
privateKey, _ := utils.LoadPrivateKeyFromFile("../testing/private_key")
|
|
||||||
opm := GetOpenAuthenticationChannelMessage()
|
opm := GetOpenAuthenticationChannelMessage()
|
||||||
authHandler := new(HiddenServiceAuthChannel)
|
authHandler := new(HiddenServiceAuthChannel)
|
||||||
authHandler.PrivateKey = privateKey
|
|
||||||
channel := Channel{ID: 1}
|
channel := Channel{ID: 1}
|
||||||
response, err := authHandler.OpenInbound(&channel, opm)
|
response, err := authHandler.OpenInbound(&channel, opm)
|
||||||
|
|
||||||
|
@ -91,9 +90,7 @@ func TestAuthenticationOpenInbound(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticationOpenOutbound(t *testing.T) {
|
func TestAuthenticationOpenOutbound(t *testing.T) {
|
||||||
privateKey, _ := utils.LoadPrivateKeyFromFile("../testing/private_key")
|
|
||||||
authHandler := new(HiddenServiceAuthChannel)
|
authHandler := new(HiddenServiceAuthChannel)
|
||||||
authHandler.PrivateKey = privateKey
|
|
||||||
channel := Channel{ID: 1}
|
channel := Channel{ID: 1}
|
||||||
response, err := authHandler.OpenOutbound(&channel)
|
response, err := authHandler.OpenOutbound(&channel)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"github.com/s-rah/go-ricochet/channels"
|
"github.com/s-rah/go-ricochet/channels"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"github.com/s-rah/go-ricochet/utils"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutoConnectionHandler implements the ConnectionHandler interface on behalf of
|
// AutoConnectionHandler implements the ConnectionHandler interface on behalf of
|
||||||
|
@ -23,9 +24,7 @@ type AutoConnectionHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init ...
|
// Init ...
|
||||||
// TODO: Split this into client and server init
|
|
||||||
func (ach *AutoConnectionHandler) Init(privateKey *rsa.PrivateKey, serverHostname string) {
|
func (ach *AutoConnectionHandler) Init(privateKey *rsa.PrivateKey, serverHostname string) {
|
||||||
|
|
||||||
ach.handlerMap = make(map[string]func() channels.Handler)
|
ach.handlerMap = make(map[string]func() channels.Handler)
|
||||||
ach.RegisterChannelHandler("im.ricochet.auth.hidden-service", func() channels.Handler {
|
ach.RegisterChannelHandler("im.ricochet.auth.hidden-service", func() channels.Handler {
|
||||||
hsau := new(channels.HiddenServiceAuthChannel)
|
hsau := new(channels.HiddenServiceAuthChannel)
|
||||||
|
@ -58,6 +57,7 @@ func (ach *AutoConnectionHandler) WaitForAuthenticationEvent() channels.AuthChan
|
||||||
|
|
||||||
// ClientAuthResult ...
|
// ClientAuthResult ...
|
||||||
func (ach *AutoConnectionHandler) ClientAuthResult(accepted bool, isKnownContact bool) {
|
func (ach *AutoConnectionHandler) ClientAuthResult(accepted bool, isKnownContact bool) {
|
||||||
|
log.Printf("Got auth result %v %v", accepted, isKnownContact)
|
||||||
ach.authResultChannel <- channels.AuthChannelResult{Accepted: accepted, IsKnownContact: isKnownContact}
|
ach.authResultChannel <- channels.AuthChannelResult{Accepted: accepted, IsKnownContact: isKnownContact}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ func (ach *AutoConnectionHandler) ClientAuthResult(accepted bool, isKnownContact
|
||||||
func (ach *AutoConnectionHandler) ServerAuthValid(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
|
func (ach *AutoConnectionHandler) ServerAuthValid(hostname string, publicKey rsa.PublicKey) (allowed, known bool) {
|
||||||
// Do something
|
// Do something
|
||||||
accepted, isKnownContact := ach.sach(hostname, publicKey)
|
accepted, isKnownContact := ach.sach(hostname, publicKey)
|
||||||
ach.authResultChannel <- channels.AuthChannelResult{Hostname: hostname, Accepted: accepted, IsKnownContact: isKnownContact}
|
ach.authResultChannel <- channels.AuthChannelResult{Accepted: accepted, IsKnownContact: isKnownContact}
|
||||||
return accepted, isKnownContact
|
return accepted, isKnownContact
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,7 @@ func (ach *AutoConnectionHandler) OnOpenChannelRequest(ctype string) (channels.H
|
||||||
handler, ok := ach.handlerMap[ctype]
|
handler, ok := ach.handlerMap[ctype]
|
||||||
if ok {
|
if ok {
|
||||||
h := handler()
|
h := handler()
|
||||||
|
log.Printf("Got Channel Handler")
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
return nil, utils.UnknownChannelTypeError
|
return nil, utils.UnknownChannelTypeError
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package connection
|
package connection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/s-rah/go-ricochet/channels"
|
"github.com/s-rah/go-ricochet/channels"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChannelManager encapsulates the logic for server and client side assignment
|
// ChannelManager encapsulates the logic for server and client side assignment
|
||||||
|
@ -38,7 +38,7 @@ func NewServerChannelManager() *ChannelManager {
|
||||||
func (cm *ChannelManager) OpenChannelRequest(chandler channels.Handler) (*channels.Channel, error) {
|
func (cm *ChannelManager) OpenChannelRequest(chandler channels.Handler) (*channels.Channel, error) {
|
||||||
// Some channels only allow us to open one of them per connection
|
// Some channels only allow us to open one of them per connection
|
||||||
if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Outbound) != nil {
|
if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Outbound) != nil {
|
||||||
return nil, utils.AttemptToOpenMoreThanOneSingletonChannelError
|
return nil, errors.New("Connection already has channel of type " + chandler.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := new(channels.Channel)
|
channel := new(channels.Channel)
|
||||||
|
@ -57,20 +57,20 @@ func (cm *ChannelManager) OpenChannelRequest(chandler channels.Handler) (*channe
|
||||||
func (cm *ChannelManager) OpenChannelRequestFromPeer(channelID int32, chandler channels.Handler) (*channels.Channel, error) {
|
func (cm *ChannelManager) OpenChannelRequestFromPeer(channelID int32, chandler channels.Handler) (*channels.Channel, error) {
|
||||||
if cm.isClient && (channelID%2) != 0 {
|
if cm.isClient && (channelID%2) != 0 {
|
||||||
// Server is trying to open odd numbered channels
|
// Server is trying to open odd numbered channels
|
||||||
return nil, utils.ServerAttemptedToOpenEvenNumberedChannelError
|
return nil, errors.New("server may only open even numbered channels")
|
||||||
} else if !cm.isClient && (channelID%2) == 0 {
|
} else if !cm.isClient && (channelID%2) == 0 {
|
||||||
// Server is trying to open odd numbered channels
|
// Server is trying to open odd numbered channels
|
||||||
return nil, utils.ClientAttemptedToOpenOddNumberedChannelError
|
return nil, errors.New("client may only open odd numbered channels")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, exists := cm.channels[channelID]
|
_, exists := cm.channels[channelID]
|
||||||
if exists {
|
if exists {
|
||||||
return nil, utils.ChannelIDIsAlreadyInUseError
|
return nil, errors.New("channel id is already in use")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some channels only allow us to open one of them per connection
|
// Some channels only allow us to open one of them per connection
|
||||||
if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Inbound) != nil {
|
if chandler.Singleton() && cm.Channel(chandler.Type(), channels.Inbound) != nil {
|
||||||
return nil, utils.AttemptToOpenMoreThanOneSingletonChannelError
|
return nil, errors.New("Connection already has channel of type " + chandler.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := new(channels.Channel)
|
channel := new(channels.Channel)
|
||||||
|
|
|
@ -2,13 +2,13 @@ package connection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/s-rah/go-ricochet/channels"
|
"github.com/s-rah/go-ricochet/channels"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"github.com/s-rah/go-ricochet/utils"
|
||||||
"github.com/s-rah/go-ricochet/wire/control"
|
"github.com/s-rah/go-ricochet/wire/control"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Connection encapsulates the state required to maintain a connection to
|
// Connection encapsulates the state required to maintain a connection to
|
||||||
|
@ -29,7 +29,6 @@ type Connection struct {
|
||||||
unlockResponseChannel chan bool
|
unlockResponseChannel chan bool
|
||||||
|
|
||||||
messageBuilder utils.MessageBuilder
|
messageBuilder utils.MessageBuilder
|
||||||
trace bool
|
|
||||||
|
|
||||||
Conn io.ReadWriteCloser
|
Conn io.ReadWriteCloser
|
||||||
IsInbound bool
|
IsInbound bool
|
||||||
|
@ -75,10 +74,6 @@ func NewOutboundConnection(conn io.ReadWriteCloser, remoteHostname string) *Conn
|
||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *Connection) TraceLog(enabled bool) {
|
|
||||||
rc.trace = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// start
|
// start
|
||||||
func (rc *Connection) start() {
|
func (rc *Connection) start() {
|
||||||
for {
|
for {
|
||||||
|
@ -96,11 +91,11 @@ func (rc *Connection) start() {
|
||||||
// use Do()
|
// use Do()
|
||||||
func (rc *Connection) Do(do func() error) error {
|
func (rc *Connection) Do(do func() error) error {
|
||||||
// Force process to soft-break so we can lock
|
// Force process to soft-break so we can lock
|
||||||
rc.traceLog("request unlocking of process loop for do()")
|
log.Printf("UnLocking Processloop")
|
||||||
rc.unlockChannel <- true
|
rc.unlockChannel <- true
|
||||||
rc.traceLog("process loop is unlocked for do()")
|
log.Printf("Unlocked Processloop")
|
||||||
ret := do()
|
ret := do()
|
||||||
rc.traceLog("giving up lock process loop after do() ")
|
log.Printf("Giving up lock Processloop")
|
||||||
rc.unlockResponseChannel <- true
|
rc.unlockResponseChannel <- true
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
@ -110,12 +105,10 @@ func (rc *Connection) Do(do func() error) error {
|
||||||
// are not met on the local side (a nill error return does not mean the
|
// are not met on the local side (a nill error return does not mean the
|
||||||
// channel was opened successfully)
|
// channel was opened successfully)
|
||||||
func (rc *Connection) RequestOpenChannel(ctype string, handler Handler) error {
|
func (rc *Connection) RequestOpenChannel(ctype string, handler Handler) error {
|
||||||
rc.traceLog(fmt.Sprintf("requesting open channel of type %s", ctype))
|
|
||||||
return rc.Do(func() error {
|
return rc.Do(func() error {
|
||||||
chandler, err := handler.OnOpenChannelRequest(ctype)
|
chandler, err := handler.OnOpenChannelRequest(ctype)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc.traceLog(fmt.Sprintf("failed to request open channel of type %v", err))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,14 +117,13 @@ func (rc *Connection) RequestOpenChannel(ctype string, handler Handler) error {
|
||||||
// Enforce Authentication Check.
|
// Enforce Authentication Check.
|
||||||
_, authed := rc.Authentication[chandler.RequiresAuthentication()]
|
_, authed := rc.Authentication[chandler.RequiresAuthentication()]
|
||||||
if !authed {
|
if !authed {
|
||||||
return utils.UnauthorizedActionError
|
return errors.New("connection is not auth'd")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
channel, err := rc.channelManager.OpenChannelRequest(chandler)
|
channel, err := rc.channelManager.OpenChannelRequest(chandler)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc.traceLog(fmt.Sprintf("failed to reqeust open channel of type %v", err))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,10 +139,8 @@ func (rc *Connection) RequestOpenChannel(ctype string, handler Handler) error {
|
||||||
}
|
}
|
||||||
response, err := chandler.OpenOutbound(channel)
|
response, err := chandler.OpenOutbound(channel)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rc.traceLog(fmt.Sprintf("requested open channel of type %s", ctype))
|
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, response)
|
rc.SendRicochetPacket(rc.Conn, 0, response)
|
||||||
} else {
|
} else {
|
||||||
rc.traceLog(fmt.Sprintf("failed to reqeust open channel of type %v", err))
|
|
||||||
rc.channelManager.RemoveChannel(channel.ID)
|
rc.channelManager.RemoveChannel(channel.ID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -167,18 +157,19 @@ func (rc *Connection) RequestOpenChannel(ctype string, handler Handler) error {
|
||||||
// Process blocks until the connection is closed or until Break() is called.
|
// Process blocks until the connection is closed or until Break() is called.
|
||||||
// If the connection is closed, a non-nil error is returned.
|
// If the connection is closed, a non-nil error is returned.
|
||||||
func (rc *Connection) Process(handler Handler) error {
|
func (rc *Connection) Process(handler Handler) error {
|
||||||
rc.traceLog("entering process loop")
|
log.Printf("Entering Processloop")
|
||||||
handler.OnReady(rc)
|
handler.OnReady(rc)
|
||||||
breaked := false
|
breaked := false
|
||||||
for !breaked {
|
for !breaked {
|
||||||
|
|
||||||
var packet utils.RicochetData
|
var packet utils.RicochetData
|
||||||
|
tick := time.Tick(30 * time.Second)
|
||||||
select {
|
select {
|
||||||
case <-rc.unlockChannel:
|
case <-rc.unlockChannel:
|
||||||
<-rc.unlockResponseChannel
|
<-rc.unlockResponseChannel
|
||||||
continue
|
continue
|
||||||
case <-rc.breakChannel:
|
case <-rc.breakChannel:
|
||||||
rc.traceLog("process has ended after break")
|
log.Printf("Process has Ended as Expected!!!")
|
||||||
breaked = true
|
breaked = true
|
||||||
continue
|
continue
|
||||||
case packet = <-rc.packetChannel:
|
case packet = <-rc.packetChannel:
|
||||||
|
@ -187,10 +178,14 @@ func (rc *Connection) Process(handler Handler) error {
|
||||||
rc.Conn.Close()
|
rc.Conn.Close()
|
||||||
handler.OnClosed(err)
|
handler.OnClosed(err)
|
||||||
return err
|
return err
|
||||||
|
case <-tick:
|
||||||
|
log.Printf("timeout")
|
||||||
|
return errors.New("peer timed out")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Received Packet on Channel %d", packet.Channel)
|
||||||
|
|
||||||
if packet.Channel == 0 {
|
if packet.Channel == 0 {
|
||||||
rc.traceLog(fmt.Sprintf("received control packet on channel %d", packet.Channel))
|
|
||||||
res := new(Protocol_Data_Control.Packet)
|
res := new(Protocol_Data_Control.Packet)
|
||||||
err := proto.Unmarshal(packet.Data[:], res)
|
err := proto.Unmarshal(packet.Data[:], res)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -201,11 +196,9 @@ func (rc *Connection) Process(handler Handler) error {
|
||||||
channel, found := rc.channelManager.GetChannel(packet.Channel)
|
channel, found := rc.channelManager.GetChannel(packet.Channel)
|
||||||
if found {
|
if found {
|
||||||
if len(packet.Data) == 0 {
|
if len(packet.Data) == 0 {
|
||||||
rc.traceLog(fmt.Sprintf("removing channel %d", packet.Channel))
|
|
||||||
rc.channelManager.RemoveChannel(packet.Channel)
|
rc.channelManager.RemoveChannel(packet.Channel)
|
||||||
(*channel.Handler).Closed(utils.ChannelClosedByPeerError)
|
(*channel.Handler).Closed(errors.New("channel closed by peer"))
|
||||||
} else {
|
} else {
|
||||||
rc.traceLog(fmt.Sprintf("received packet on %v channel %d", (*channel.Handler).Type(), packet.Channel))
|
|
||||||
// Send The Ricochet Packet to the Handler
|
// Send The Ricochet Packet to the Handler
|
||||||
(*channel.Handler).Packet(packet.Data[:])
|
(*channel.Handler).Packet(packet.Data[:])
|
||||||
}
|
}
|
||||||
|
@ -213,7 +206,6 @@ func (rc *Connection) Process(handler Handler) error {
|
||||||
// When a non-zero packet is received for an unknown
|
// When a non-zero packet is received for an unknown
|
||||||
// channel, the recipient responds by closing
|
// channel, the recipient responds by closing
|
||||||
// that channel.
|
// that channel.
|
||||||
rc.traceLog(fmt.Sprintf("received packet on unknown channel %d. closing.", packet.Channel))
|
|
||||||
if len(packet.Data) != 0 {
|
if len(packet.Data) != 0 {
|
||||||
rc.SendRicochetPacket(rc.Conn, packet.Channel, []byte{})
|
rc.SendRicochetPacket(rc.Conn, packet.Channel, []byte{})
|
||||||
}
|
}
|
||||||
|
@ -242,15 +234,12 @@ func (rc *Connection) controlPacket(handler Handler, res *Protocol_Data_Control.
|
||||||
|
|
||||||
// Check that we have the authentication already
|
// Check that we have the authentication already
|
||||||
if chandler.RequiresAuthentication() != "none" {
|
if chandler.RequiresAuthentication() != "none" {
|
||||||
rc.traceLog(fmt.Sprintf("channel %v requires authorization of type %v", chandler.Type(), chandler.RequiresAuthentication()))
|
|
||||||
// Enforce Authentication Check.
|
// Enforce Authentication Check.
|
||||||
_, authed := rc.Authentication[chandler.RequiresAuthentication()]
|
_, authed := rc.Authentication[chandler.RequiresAuthentication()]
|
||||||
if !authed {
|
if !authed {
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, []byte{})
|
rc.SendRicochetPacket(rc.Conn, 0, []byte{})
|
||||||
rc.traceLog(fmt.Sprintf("do not have required authorization to open channel type %v", chandler.Type()))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rc.traceLog("succeeded authorization check")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channel, err := rc.channelManager.OpenChannelRequestFromPeer(opm.GetChannelIdentifier(), chandler)
|
channel, err := rc.channelManager.OpenChannelRequestFromPeer(opm.GetChannelIdentifier(), chandler)
|
||||||
|
@ -270,17 +259,15 @@ func (rc *Connection) controlPacket(handler Handler, res *Protocol_Data_Control.
|
||||||
|
|
||||||
response, err := chandler.OpenInbound(channel, opm)
|
response, err := chandler.OpenInbound(channel, opm)
|
||||||
if err == nil && channel.Pending == false {
|
if err == nil && channel.Pending == false {
|
||||||
rc.traceLog(fmt.Sprintf("opening channel %v on %v", channel.Type, channel.ID))
|
log.Printf("Opening Channel %v on %v", channel.Type, channel.ID)
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, response)
|
rc.SendRicochetPacket(rc.Conn, 0, response)
|
||||||
} else {
|
} else {
|
||||||
rc.traceLog(fmt.Sprintf("removing channel %v", channel.ID))
|
|
||||||
rc.channelManager.RemoveChannel(channel.ID)
|
rc.channelManager.RemoveChannel(channel.ID)
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, []byte{})
|
rc.SendRicochetPacket(rc.Conn, 0, []byte{})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Send Error Packet
|
// Send Error Packet
|
||||||
response := rc.messageBuilder.RejectOpenChannel(opm.GetChannelIdentifier(), "GenericError")
|
response := rc.messageBuilder.RejectOpenChannel(opm.GetChannelIdentifier(), "GenericError")
|
||||||
rc.traceLog(fmt.Sprintf("sending reject open channel for %v", opm.GetChannelIdentifier()))
|
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, response)
|
rc.SendRicochetPacket(rc.Conn, 0, response)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -291,15 +278,12 @@ func (rc *Connection) controlPacket(handler Handler, res *Protocol_Data_Control.
|
||||||
channel, found := rc.channelManager.GetChannel(id)
|
channel, found := rc.channelManager.GetChannel(id)
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
rc.traceLog(fmt.Sprintf("channel result recived for unknown channel: %v", channel.Type, id))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cr.GetOpened() {
|
if cr.GetOpened() {
|
||||||
rc.traceLog(fmt.Sprintf("channel of type %v opened on %v", channel.Type, id))
|
|
||||||
(*channel.Handler).OpenOutboundResult(nil, cr)
|
(*channel.Handler).OpenOutboundResult(nil, cr)
|
||||||
} else {
|
} else {
|
||||||
rc.traceLog(fmt.Sprintf("channel of type %v rejected on %v", channel.Type, id))
|
|
||||||
(*channel.Handler).OpenOutboundResult(errors.New(""), cr)
|
(*channel.Handler).OpenOutboundResult(errors.New(""), cr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,35 +291,25 @@ func (rc *Connection) controlPacket(handler Handler, res *Protocol_Data_Control.
|
||||||
// XXX Though not currently part of the protocol
|
// XXX Though not currently part of the protocol
|
||||||
// We should likely put these calls behind
|
// We should likely put these calls behind
|
||||||
// authentication.
|
// authentication.
|
||||||
rc.traceLog("received keep alive packet")
|
|
||||||
if res.GetKeepAlive().GetResponseRequested() {
|
if res.GetKeepAlive().GetResponseRequested() {
|
||||||
messageBuilder := new(utils.MessageBuilder)
|
messageBuilder := new(utils.MessageBuilder)
|
||||||
raw := messageBuilder.KeepAlive(true)
|
raw := messageBuilder.KeepAlive(true)
|
||||||
rc.traceLog("sending keep alive response")
|
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, raw)
|
rc.SendRicochetPacket(rc.Conn, 0, raw)
|
||||||
}
|
}
|
||||||
} else if res.GetEnableFeatures() != nil {
|
} else if res.GetEnableFeatures() != nil {
|
||||||
rc.traceLog("received features enabled packet")
|
// TODO Respond with an Empty List
|
||||||
messageBuilder := new(utils.MessageBuilder)
|
messageBuilder := new(utils.MessageBuilder)
|
||||||
raw := messageBuilder.FeaturesEnabled([]string{})
|
raw := messageBuilder.FeaturesEnabled([]string{})
|
||||||
rc.traceLog("sending featured enabled empty response")
|
|
||||||
rc.SendRicochetPacket(rc.Conn, 0, raw)
|
rc.SendRicochetPacket(rc.Conn, 0, raw)
|
||||||
} else if res.GetFeaturesEnabled() != nil {
|
} else if res.GetFeaturesEnabled() != nil {
|
||||||
// TODO We should never send out an enabled features
|
// TODO We should never send out an enabled features
|
||||||
// request.
|
// request.
|
||||||
rc.traceLog("sending unsolicited features enabled response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *Connection) traceLog(message string) {
|
|
||||||
if rc.trace {
|
|
||||||
log.Printf(message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Break causes Process() to return, but does not close the underlying connection
|
// Break causes Process() to return, but does not close the underlying connection
|
||||||
func (rc *Connection) Break() {
|
func (rc *Connection) Break() {
|
||||||
rc.traceLog("breaking out of process loop")
|
log.Printf("breaking...")
|
||||||
rc.breakChannel <- true
|
rc.breakChannel <- true
|
||||||
<-rc.breakResultChannel // Wait for Process to End
|
<-rc.breakResultChannel // Wait for Process to End
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ func TestProcessAuthAsServer(t *testing.T) {
|
||||||
cconn, _ := net.Dial("tcp", ln.Addr().String())
|
cconn, _ := net.Dial("tcp", ln.Addr().String())
|
||||||
|
|
||||||
orc := NewOutboundConnection(cconn, "kwke2hntvyfqm7dr")
|
orc := NewOutboundConnection(cconn, "kwke2hntvyfqm7dr")
|
||||||
orc.TraceLog(true)
|
|
||||||
privateKey, _ := utils.LoadPrivateKeyFromFile("../testing/private_key")
|
privateKey, _ := utils.LoadPrivateKeyFromFile("../testing/private_key")
|
||||||
|
|
||||||
known, err := HandleOutboundConnection(orc).ProcessAuthAsClient(privateKey)
|
known, err := HandleOutboundConnection(orc).ProcessAuthAsClient(privateKey)
|
||||||
|
|
|
@ -35,10 +35,6 @@ func HandleInboundConnection(c *Connection) *InboundConnectionHandler {
|
||||||
// assume they are required to send a contact request before any other activity.
|
// assume they are required to send a contact request before any other activity.
|
||||||
func (ich *InboundConnectionHandler) ProcessAuthAsServer(privateKey *rsa.PrivateKey, sach func(hostname string, publicKey rsa.PublicKey) (allowed, known bool)) error {
|
func (ich *InboundConnectionHandler) ProcessAuthAsServer(privateKey *rsa.PrivateKey, sach func(hostname string, publicKey rsa.PublicKey) (allowed, known bool)) error {
|
||||||
|
|
||||||
if privateKey == nil {
|
|
||||||
return utils.PrivateKeyNotSetError
|
|
||||||
}
|
|
||||||
|
|
||||||
ach := new(AutoConnectionHandler)
|
ach := new(AutoConnectionHandler)
|
||||||
ach.Init(privateKey, ich.connection.RemoteHostname)
|
ach.Init(privateKey, ich.connection.RemoteHostname)
|
||||||
ach.SetServerAuthHandler(sach)
|
ach.SetServerAuthHandler(sach)
|
||||||
|
@ -56,7 +52,6 @@ func (ich *InboundConnectionHandler) ProcessAuthAsServer(privateKey *rsa.Private
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if authResult.Accepted == true {
|
if authResult.Accepted == true {
|
||||||
ich.connection.RemoteHostname = authResult.Hostname
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return utils.ClientFailedToAuthenticateError
|
return utils.ClientFailedToAuthenticateError
|
||||||
|
|
|
@ -2,9 +2,10 @@ package connection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"errors"
|
||||||
"github.com/s-rah/go-ricochet/channels"
|
"github.com/s-rah/go-ricochet/channels"
|
||||||
"github.com/s-rah/go-ricochet/policies"
|
"github.com/s-rah/go-ricochet/policies"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OutboundConnectionHandler is a convieniance wrapper for handling outbound
|
// OutboundConnectionHandler is a convieniance wrapper for handling outbound
|
||||||
|
@ -32,11 +33,6 @@ func HandleOutboundConnection(c *Connection) *OutboundConnectionHandler {
|
||||||
// accepts us as a known contact. Unknown contacts will generally need to send a contact
|
// accepts us as a known contact. Unknown contacts will generally need to send a contact
|
||||||
// request before any other activity.
|
// request before any other activity.
|
||||||
func (och *OutboundConnectionHandler) ProcessAuthAsClient(privateKey *rsa.PrivateKey) (bool, error) {
|
func (och *OutboundConnectionHandler) ProcessAuthAsClient(privateKey *rsa.PrivateKey) (bool, error) {
|
||||||
|
|
||||||
if privateKey == nil {
|
|
||||||
return false, utils.PrivateKeyNotSetError
|
|
||||||
}
|
|
||||||
|
|
||||||
ach := new(AutoConnectionHandler)
|
ach := new(AutoConnectionHandler)
|
||||||
ach.Init(privateKey, och.connection.RemoteHostname)
|
ach.Init(privateKey, och.connection.RemoteHostname)
|
||||||
|
|
||||||
|
@ -46,7 +42,9 @@ func (och *OutboundConnectionHandler) ProcessAuthAsClient(privateKey *rsa.Privat
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("waiting for auth result")
|
||||||
result = ach.WaitForAuthenticationEvent()
|
result = ach.WaitForAuthenticationEvent()
|
||||||
|
log.Printf("received auth result")
|
||||||
och.connection.Break()
|
och.connection.Break()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -60,5 +58,5 @@ func (och *OutboundConnectionHandler) ProcessAuthAsClient(privateKey *rsa.Privat
|
||||||
return result.IsKnownContact, nil
|
return result.IsKnownContact, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, utils.ServerRejectedClientConnectionError
|
return false, errors.New("authentication was not accepted by the server")
|
||||||
}
|
}
|
||||||
|
|
49
ricochet.go
49
ricochet.go
|
@ -1,12 +1,12 @@
|
||||||
package goricochet
|
package goricochet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/s-rah/go-ricochet/connection"
|
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"github.com/s-rah/go-ricochet/utils"
|
||||||
|
"github.com/s-rah/go-ricochet/connection"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open establishes a protocol session on an established net.Conn, and returns a new
|
// Open establishes a protocol session on an established net.Conn, and returns a new
|
||||||
// OpenConnection instance representing this connection. On error, the connection
|
// OpenConnection instance representing this connection. On error, the connection
|
||||||
// will be closed. This function blocks until version negotiation has completed.
|
// will be closed. This function blocks until version negotiation has completed.
|
||||||
|
@ -14,20 +14,24 @@ import (
|
||||||
// handling protocol messages.
|
// handling protocol messages.
|
||||||
func Open(remoteHostname string) (*connection.Connection, error) {
|
func Open(remoteHostname string) (*connection.Connection, error) {
|
||||||
networkResolver := utils.NetworkResolver{}
|
networkResolver := utils.NetworkResolver{}
|
||||||
|
log.Printf("Connecting...")
|
||||||
conn, remoteHostname, err := networkResolver.Resolve(remoteHostname)
|
conn, remoteHostname, err := networkResolver.Resolve(remoteHostname)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Connected...negotiating version")
|
||||||
rc, err := negotiateVersion(conn, remoteHostname)
|
rc, err := negotiateVersion(conn, remoteHostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Printf("Connected...negotiated version")
|
||||||
return rc, nil
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// negotiate version takes an open network connection and executes
|
// negotiate version takes an open network connection and executes
|
||||||
// the ricochet version negotiation procedure.
|
// the ricochet version negotiation procedure.
|
||||||
func negotiateVersion(conn net.Conn, remoteHostname string) (*connection.Connection, error) {
|
func negotiateVersion(conn net.Conn, remoteHostname string) (*connection.Connection, error) {
|
||||||
|
@ -44,47 +48,8 @@ func negotiateVersion(conn net.Conn, remoteHostname string) (*connection.Connect
|
||||||
if res[0] != 0x01 {
|
if res[0] != 0x01 {
|
||||||
return nil, utils.VersionNegotiationFailed
|
return nil, utils.VersionNegotiationFailed
|
||||||
}
|
}
|
||||||
rc := connection.NewOutboundConnection(conn, remoteHostname)
|
rc := connection.NewOutboundConnection(conn,remoteHostname)
|
||||||
return rc, nil
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NegotiateVersionInbound takes in a connection and performs version negotiation
|
|
||||||
// as if that connection was a client. Returns a ricochet connection if successful
|
|
||||||
// error otherwise.
|
|
||||||
func NegotiateVersionInbound(conn net.Conn) (*connection.Connection, error) {
|
|
||||||
versions := []byte{0x49, 0x4D, 0x01, 0x01}
|
|
||||||
// Read version response header
|
|
||||||
header := make([]byte, 3)
|
|
||||||
if _, err := io.ReadAtLeast(conn, header, len(header)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if header[0] != versions[0] || header[1] != versions[1] || header[2] < 1 {
|
|
||||||
return nil, utils.VersionNegotiationError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read list of supported versions (which is header[2] bytes long)
|
|
||||||
versionList := make([]byte, header[2])
|
|
||||||
if _, err := io.ReadAtLeast(conn, versionList, len(versionList)); err != nil {
|
|
||||||
return nil, utils.VersionNegotiationError
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedVersion := byte(0xff)
|
|
||||||
for _, v := range versionList {
|
|
||||||
if v == 0x01 {
|
|
||||||
selectedVersion = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if n, err := conn.Write([]byte{selectedVersion}); err != nil || n < 1 {
|
|
||||||
return nil, utils.VersionNegotiationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
if selectedVersion == 0xff {
|
|
||||||
return nil, utils.VersionNegotiationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
rc := connection.NewInboundConnection(conn)
|
|
||||||
return rc, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
package goricochet
|
package goricochet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
"github.com/s-rah/go-ricochet/utils"
|
"github.com/s-rah/go-ricochet/utils"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func SimpleServer() {
|
func SimpleServer() {
|
||||||
ln, _ := net.Listen("tcp", "127.0.0.1:11000")
|
ln,_ := net.Listen("tcp", "127.0.0.1:11000")
|
||||||
conn, _ := ln.Accept()
|
conn,_ := ln.Accept()
|
||||||
b := make([]byte, 4)
|
b := make([]byte, 4)
|
||||||
n, err := conn.Read(b)
|
n,err := conn.Read(b)
|
||||||
if n == 4 && err == nil {
|
if n == 4 && err == nil {
|
||||||
conn.Write([]byte{0x01})
|
conn.Write([]byte{0x01})
|
||||||
}
|
}
|
||||||
|
@ -19,16 +20,16 @@ func SimpleServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func BadVersionNegotiation() {
|
func BadVersionNegotiation() {
|
||||||
ln, _ := net.Listen("tcp", "127.0.0.1:11001")
|
ln,_ := net.Listen("tcp", "127.0.0.1:11001")
|
||||||
conn, _ := ln.Accept()
|
conn,_ := ln.Accept()
|
||||||
// We are already testing negotiation bytes, we don't care, just send a termination.
|
// We are already testing negotiation bytes, we don't care, just send a termination.
|
||||||
conn.Write([]byte{0x00})
|
conn.Write([]byte{0x00})
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotRicochetServer() {
|
func NotRicochetServer() {
|
||||||
ln, _ := net.Listen("tcp", "127.0.0.1:11002")
|
ln,_ := net.Listen("tcp", "127.0.0.1:11002")
|
||||||
conn, _ := ln.Accept()
|
conn,_ := ln.Accept()
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ func TestRicochet(t *testing.T) {
|
||||||
// Wait for Server to Initialize
|
// Wait for Server to Initialize
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
rc, err := Open("127.0.0.1:11000|abcdefghijklmno.onion")
|
rc,err := Open("127.0.0.1:11000|abcdefghijklmno.onion")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if rc.IsInbound {
|
if rc.IsInbound {
|
||||||
t.Errorf("RicochetConnection declares itself as an Inbound connection after an Outbound attempt...that shouldn't happen")
|
t.Errorf("RicochetConnection declares itself as an Inbound connection after an Outbound attempt...that shouldn't happen")
|
||||||
|
@ -47,21 +48,22 @@ func TestRicochet(t *testing.T) {
|
||||||
t.Errorf("RicochetProtocol: Open Failed: %v", err)
|
t.Errorf("RicochetProtocol: Open Failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBadVersionNegotiation(t *testing.T) {
|
func TestBadVersionNegotiation(t*testing.T) {
|
||||||
go BadVersionNegotiation()
|
go BadVersionNegotiation()
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
_, err := Open("127.0.0.1:11001|abcdefghijklmno.onion")
|
_,err := Open("127.0.0.1:11001|abcdefghijklmno.onion")
|
||||||
if err != utils.VersionNegotiationFailed {
|
if err != utils.VersionNegotiationFailed {
|
||||||
t.Errorf("RicochetProtocol: Server Had No Correct Version - Should Have Failed: err = %v", err)
|
t.Errorf("RicochetProtocol: Server Had No Correct Version - Should Have Failed: err = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotARicochetServer(t *testing.T) {
|
|
||||||
|
func TestNotARicochetServer(t*testing.T) {
|
||||||
go NotRicochetServer()
|
go NotRicochetServer()
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
_, err := Open("127.0.0.1:11002|abcdefghijklmno.onion")
|
_,err := Open("127.0.0.1:11002|abcdefghijklmno.onion")
|
||||||
if err != utils.VersionNegotiationError {
|
if err != utils.VersionNegotiationError {
|
||||||
t.Errorf("RicochetProtocol: Server Had No Correct Version - Should Have Failed: err = %v", err)
|
t.Errorf("RicochetProtocol: Server Had No Correct Version - Should Have Failed: err = %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,53 +4,22 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"io/ioutil"
|
|
||||||
"errors"
|
"errors"
|
||||||
"crypto/rand"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
InvalidPrivateKeyFileError = Error("InvalidPrivateKeyFileError")
|
|
||||||
RICOCHET_KEY_SIZE = 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate a private key for use
|
|
||||||
func GeneratePrivateKey() (*rsa.PrivateKey, error) {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, RICOCHET_KEY_SIZE)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Could not generate key: " + err.Error())
|
|
||||||
}
|
|
||||||
privateKeyDer := x509.MarshalPKCS1PrivateKey(privateKey)
|
|
||||||
return x509.ParsePKCS1PrivateKey(privateKeyDer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPrivateKeyFromFile loads a private key from a file...
|
// LoadPrivateKeyFromFile loads a private key from a file...
|
||||||
func LoadPrivateKeyFromFile(filename string) (*rsa.PrivateKey, error) {
|
func LoadPrivateKeyFromFile(filename string) (*rsa.PrivateKey, error) {
|
||||||
pemData, err := ioutil.ReadFile(filename)
|
pemData, err := ioutil.ReadFile(filename)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParsePrivateKey(pemData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a private key string to a usable private key
|
|
||||||
func ParsePrivateKey(pemData []byte) (*rsa.PrivateKey, error) {
|
|
||||||
block, _ := pem.Decode(pemData)
|
block, _ := pem.Decode(pemData)
|
||||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||||
return nil, InvalidPrivateKeyFileError
|
return nil, errors.New("not a private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// turn a private key into storable string
|
|
||||||
func PrivateKeyToString(privateKey *rsa.PrivateKey) string {
|
|
||||||
privateKeyBlock := pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Headers: nil,
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(pem.EncodeToMemory(&privateKeyBlock))
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error captures various common ricochet errors
|
// Error captures various common ricochet errors
|
||||||
|
@ -20,27 +21,21 @@ const (
|
||||||
UnknownChannelTypeError = Error("UnknownChannelTypeError")
|
UnknownChannelTypeError = Error("UnknownChannelTypeError")
|
||||||
UnauthorizedChannelTypeError = Error("UnauthorizedChannelTypeError")
|
UnauthorizedChannelTypeError = Error("UnauthorizedChannelTypeError")
|
||||||
|
|
||||||
// Timeout Errors
|
|
||||||
ActionTimedOutError = Error("ActionTimedOutError")
|
ActionTimedOutError = Error("ActionTimedOutError")
|
||||||
PeerTimedOutError = Error("PeerTimedOutError")
|
|
||||||
|
|
||||||
// Authentication Errors
|
|
||||||
ClientFailedToAuthenticateError = Error("ClientFailedToAuthenticateError")
|
ClientFailedToAuthenticateError = Error("ClientFailedToAuthenticateError")
|
||||||
ServerRejectedClientConnectionError = Error("ServerRejectedClientConnectionError")
|
|
||||||
|
|
||||||
UnauthorizedActionError = Error("UnauthorizedActionError")
|
|
||||||
ChannelClosedByPeerError = Error("ChannelClosedByPeerError")
|
|
||||||
|
|
||||||
// Channel Management Errors
|
|
||||||
ServerAttemptedToOpenEvenNumberedChannelError = Error("ServerAttemptedToOpenEvenNumberedChannelError")
|
|
||||||
ClientAttemptedToOpenOddNumberedChannelError = Error("ClientAttemptedToOpenOddNumberedChannelError")
|
|
||||||
ChannelIDIsAlreadyInUseError = Error("ChannelIDIsAlreadyInUseError")
|
|
||||||
AttemptToOpenMoreThanOneSingletonChannelError = Error("AttemptToOpenMoreThanOneSingletonChannelError")
|
|
||||||
|
|
||||||
// Library Use Errors
|
|
||||||
PrivateKeyNotSetError = Error("ClientFailedToAuthenticateError")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RecoverFromError doesn't really recover from anything....see comment below
|
||||||
|
func RecoverFromError() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// This should only really happen if there is a failure de/serializing. If
|
||||||
|
// this does happen then we currently error. In the future we might be
|
||||||
|
// able to make this nicer.
|
||||||
|
log.Fatalf("Recovered from panic() - this really shouldn't happen. Reason: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CheckError is a helper function for panicing on errors which we need to handle
|
// CheckError is a helper function for panicing on errors which we need to handle
|
||||||
// but should be very rare e.g. failures deserializing a protobuf object that
|
// but should be very rare e.g. failures deserializing a protobuf object that
|
||||||
// should only happen if there was a bug in the underlying library.
|
// should only happen if there was a bug in the underlying library.
|
||||||
|
|
|
@ -3,14 +3,10 @@ package utils
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
InvalidPacketLengthError = Error("InvalidPacketLengthError")
|
|
||||||
InvalidChannelIDError = Error("InvalidChannelIDError")
|
|
||||||
)
|
|
||||||
|
|
||||||
// RicochetData is a structure containing the raw data and the channel it the
|
// RicochetData is a structure containing the raw data and the channel it the
|
||||||
// message originated on.
|
// message originated on.
|
||||||
type RicochetData struct {
|
type RicochetData struct {
|
||||||
|
@ -40,11 +36,11 @@ type RicochetNetwork struct {
|
||||||
func (rn *RicochetNetwork) SendRicochetPacket(dst io.Writer, channel int32, data []byte) error {
|
func (rn *RicochetNetwork) SendRicochetPacket(dst io.Writer, channel int32, data []byte) error {
|
||||||
packet := make([]byte, 4+len(data))
|
packet := make([]byte, 4+len(data))
|
||||||
if len(packet) > 65535 {
|
if len(packet) > 65535 {
|
||||||
return InvalidPacketLengthError
|
return errors.New("packet too large")
|
||||||
}
|
}
|
||||||
binary.BigEndian.PutUint16(packet[0:2], uint16(len(packet)))
|
binary.BigEndian.PutUint16(packet[0:2], uint16(len(packet)))
|
||||||
if channel < 0 || channel > 65535 {
|
if channel < 0 || channel > 65535 {
|
||||||
return InvalidChannelIDError
|
return errors.New("invalid channel ID")
|
||||||
}
|
}
|
||||||
binary.BigEndian.PutUint16(packet[2:4], uint16(channel))
|
binary.BigEndian.PutUint16(packet[2:4], uint16(channel))
|
||||||
copy(packet[4:], data[:])
|
copy(packet[4:], data[:])
|
||||||
|
@ -72,7 +68,7 @@ func (rn *RicochetNetwork) RecvRicochetPacket(reader io.Reader) (RicochetData, e
|
||||||
|
|
||||||
size := int(binary.BigEndian.Uint16(header[0:2]))
|
size := int(binary.BigEndian.Uint16(header[0:2]))
|
||||||
if size < 4 {
|
if size < 4 {
|
||||||
return packet, InvalidPacketLengthError
|
return packet, errors.New("invalid packet length")
|
||||||
}
|
}
|
||||||
|
|
||||||
packet.Channel = int32(binary.BigEndian.Uint16(header[2:4]))
|
packet.Channel = int32(binary.BigEndian.Uint16(header[2:4]))
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
CannotResolveLocalTCPAddressError = Error("CannotResolveLocalTCPAddressError")
|
|
||||||
CannotDialLocalTCPAddressError = Error("CannotDialLocalTCPAddressError")
|
|
||||||
CannotDialRicochetAddressError = Error("CannotDialRicochetAddressError")
|
|
||||||
)
|
|
||||||
|
|
||||||
// NetworkResolver allows a client to resolve various hostnames to connections
|
// NetworkResolver allows a client to resolve various hostnames to connections
|
||||||
// The supported types are onions address are:
|
// The supported types are onions address are:
|
||||||
// * ricochet:jlq67qzo6s4yp3sp
|
// * ricochet:jlq67qzo6s4yp3sp
|
||||||
|
@ -26,11 +21,11 @@ func (nr *NetworkResolver) Resolve(hostname string) (net.Conn, string, error) {
|
||||||
addrParts := strings.Split(hostname, "|")
|
addrParts := strings.Split(hostname, "|")
|
||||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addrParts[0])
|
tcpAddr, err := net.ResolveTCPAddr("tcp", addrParts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", CannotResolveLocalTCPAddressError
|
return nil, "", errors.New("Cannot Resolve Local TCP Address")
|
||||||
}
|
}
|
||||||
conn, err := net.DialTCP("tcp", nil, tcpAddr)
|
conn, err := net.DialTCP("tcp", nil, tcpAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", CannotDialLocalTCPAddressError
|
return nil, "", errors.New("Cannot Dial Local TCP Address")
|
||||||
}
|
}
|
||||||
|
|
||||||
// return just the onion address, not the local override for the hostname
|
// return just the onion address, not the local override for the hostname
|
||||||
|
@ -50,7 +45,7 @@ func (nr *NetworkResolver) Resolve(hostname string) (net.Conn, string, error) {
|
||||||
|
|
||||||
conn, err := torDialer.Dial("tcp", resolvedHostname+".onion:9878")
|
conn, err := torDialer.Dial("tcp", resolvedHostname+".onion:9878")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", CannotDialRicochetAddressError
|
return nil, "", errors.New("Cannot Dial Remote Ricochet Address")
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, resolvedHostname, nil
|
return conn, resolvedHostname, nil
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
examples/basic/basic
|
|
||||||
examples/listener/listener
|
|
||||||
examples/dialer/dialer
|
|
|
@ -1,122 +0,0 @@
|
||||||
Creative Commons Legal Code
|
|
||||||
|
|
||||||
CC0 1.0 Universal
|
|
||||||
|
|
||||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
|
||||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
|
||||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
|
||||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
|
||||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
|
||||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
|
||||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
|
||||||
HEREUNDER.
|
|
||||||
|
|
||||||
Statement of Purpose
|
|
||||||
|
|
||||||
The laws of most jurisdictions throughout the world automatically confer
|
|
||||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
|
||||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
|
||||||
authorship and/or a database (each, a "Work").
|
|
||||||
|
|
||||||
Certain owners wish to permanently relinquish those rights to a Work for
|
|
||||||
the purpose of contributing to a commons of creative, cultural and
|
|
||||||
scientific works ("Commons") that the public can reliably and without fear
|
|
||||||
of later claims of infringement build upon, modify, incorporate in other
|
|
||||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
|
||||||
and for any purposes, including without limitation commercial purposes.
|
|
||||||
These owners may contribute to the Commons to promote the ideal of a free
|
|
||||||
culture and the further production of creative, cultural and scientific
|
|
||||||
works, or to gain reputation or greater distribution for their Work in
|
|
||||||
part through the use and efforts of others.
|
|
||||||
|
|
||||||
For these and/or other purposes and motivations, and without any
|
|
||||||
expectation of additional consideration or compensation, the person
|
|
||||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
|
||||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
|
||||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
|
||||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
|
||||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
|
||||||
|
|
||||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
|
||||||
protected by copyright and related or neighboring rights ("Copyright and
|
|
||||||
Related Rights"). Copyright and Related Rights include, but are not
|
|
||||||
limited to, the following:
|
|
||||||
|
|
||||||
i. the right to reproduce, adapt, distribute, perform, display,
|
|
||||||
communicate, and translate a Work;
|
|
||||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
|
||||||
iii. publicity and privacy rights pertaining to a person's image or
|
|
||||||
likeness depicted in a Work;
|
|
||||||
iv. rights protecting against unfair competition in regards to a Work,
|
|
||||||
subject to the limitations in paragraph 4(a), below;
|
|
||||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
|
||||||
in a Work;
|
|
||||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
|
||||||
European Parliament and of the Council of 11 March 1996 on the legal
|
|
||||||
protection of databases, and under any national implementation
|
|
||||||
thereof, including any amended or successor version of such
|
|
||||||
directive); and
|
|
||||||
vii. other similar, equivalent or corresponding rights throughout the
|
|
||||||
world based on applicable law or treaty, and any national
|
|
||||||
implementations thereof.
|
|
||||||
|
|
||||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
|
||||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
|
||||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
|
||||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
|
||||||
of action, whether now known or unknown (including existing as well as
|
|
||||||
future claims and causes of action), in the Work (i) in all territories
|
|
||||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
|
||||||
treaty (including future time extensions), (iii) in any current or future
|
|
||||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
|
||||||
including without limitation commercial, advertising or promotional
|
|
||||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
|
||||||
member of the public at large and to the detriment of Affirmer's heirs and
|
|
||||||
successors, fully intending that such Waiver shall not be subject to
|
|
||||||
revocation, rescission, cancellation, termination, or any other legal or
|
|
||||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
|
||||||
as contemplated by Affirmer's express Statement of Purpose.
|
|
||||||
|
|
||||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
|
||||||
be judged legally invalid or ineffective under applicable law, then the
|
|
||||||
Waiver shall be preserved to the maximum extent permitted taking into
|
|
||||||
account Affirmer's express Statement of Purpose. In addition, to the
|
|
||||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
|
||||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
|
||||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
|
||||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
|
||||||
maximum duration provided by applicable law or treaty (including future
|
|
||||||
time extensions), (iii) in any current or future medium and for any number
|
|
||||||
of copies, and (iv) for any purpose whatsoever, including without
|
|
||||||
limitation commercial, advertising or promotional purposes (the
|
|
||||||
"License"). The License shall be deemed effective as of the date CC0 was
|
|
||||||
applied by Affirmer to the Work. Should any part of the License for any
|
|
||||||
reason be judged legally invalid or ineffective under applicable law, such
|
|
||||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
|
||||||
of the License, and in such case Affirmer hereby affirms that he or she
|
|
||||||
will not (i) exercise any of his or her remaining Copyright and Related
|
|
||||||
Rights in the Work or (ii) assert any associated claims and causes of
|
|
||||||
action with respect to the Work, in either case contrary to Affirmer's
|
|
||||||
express Statement of Purpose.
|
|
||||||
|
|
||||||
4. Limitations and Disclaimers.
|
|
||||||
|
|
||||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
|
||||||
surrendered, licensed or otherwise affected by this document.
|
|
||||||
b. Affirmer offers the Work as-is and makes no representations or
|
|
||||||
warranties of any kind concerning the Work, express, implied,
|
|
||||||
statutory or otherwise, including without limitation warranties of
|
|
||||||
title, merchantability, fitness for a particular purpose, non
|
|
||||||
infringement, or the absence of latent or other defects, accuracy, or
|
|
||||||
the present or absence of errors, whether or not discoverable, all to
|
|
||||||
the greatest extent permissible under applicable law.
|
|
||||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
|
||||||
that may apply to the Work or any use thereof, including without
|
|
||||||
limitation any person's Copyright and Related Rights in the Work.
|
|
||||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
|
||||||
consents, permissions or other rights required for any use of the
|
|
||||||
Work.
|
|
||||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
|
||||||
party to this document and has no duty or obligation with respect to
|
|
||||||
this CC0 or use of the Work.
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
## bulb - Is not stem
|
|
||||||
### Yawning Angel (yawning at torproject dot org)
|
|
||||||
|
|
||||||
bulb is a Go language interface to the Tor control port. It is considerably
|
|
||||||
lighter in functionality than stem and other controller libraries, and is
|
|
||||||
intended to be used in combination with`control-spec.txt`.
|
|
||||||
|
|
||||||
It was written primarily as a not-invented-here hack, and the base interface is
|
|
||||||
more than likely to stay fairly low level, though useful helpers will be added
|
|
||||||
as I need them.
|
|
||||||
|
|
||||||
Things you should probably use instead:
|
|
||||||
* [stem](https://stem.torproject.org)
|
|
||||||
* [txtorcon](https://pypi.python.org/pypi/txtorcon)
|
|
||||||
* [orc](https://github.com/sycamoreone/orc)
|
|
||||||
|
|
||||||
Bugs:
|
|
||||||
* bulb does not send the 'QUIT' command before closing the connection.
|
|
|
@ -1,137 +0,0 @@
|
||||||
// cmd_authenticate.go - AUTHENTICATE/AUTHCHALLENGE commands.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Authenticate authenticates with the Tor instance using the "best" possible
|
|
||||||
// authentication method. The password argument is entirely optional, and will
|
|
||||||
// only be used if the "SAFECOOKE" and "NULL" authentication methods are not
|
|
||||||
// available and "HASHEDPASSWORD" is.
|
|
||||||
func (c *Conn) Authenticate(password string) error {
|
|
||||||
if c.isAuthenticated {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the supported authentication methods, and the cookie path.
|
|
||||||
pi, err := c.ProtocolInfo()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// "COOKIE" authentication exists, but anything modern supports
|
|
||||||
// "SAFECOOKIE".
|
|
||||||
const (
|
|
||||||
cmdAuthenticate = "AUTHENTICATE"
|
|
||||||
authMethodNull = "NULL"
|
|
||||||
authMethodPassword = "HASHEDPASSWORD"
|
|
||||||
authMethodSafeCookie = "SAFECOOKIE"
|
|
||||||
)
|
|
||||||
if pi.AuthMethods[authMethodNull] {
|
|
||||||
_, err = c.Request(cmdAuthenticate)
|
|
||||||
c.isAuthenticated = err == nil
|
|
||||||
return err
|
|
||||||
} else if pi.AuthMethods[authMethodSafeCookie] {
|
|
||||||
const (
|
|
||||||
authCookieLength = 32
|
|
||||||
authNonceLength = 32
|
|
||||||
authHashLength = 32
|
|
||||||
|
|
||||||
authServerHashKey = "Tor safe cookie authentication server-to-controller hash"
|
|
||||||
authClientHashKey = "Tor safe cookie authentication controller-to-server hash"
|
|
||||||
)
|
|
||||||
|
|
||||||
if pi.CookieFile == "" {
|
|
||||||
return newProtocolError("invalid (empty) COOKIEFILE")
|
|
||||||
}
|
|
||||||
cookie, err := ioutil.ReadFile(pi.CookieFile)
|
|
||||||
if err != nil {
|
|
||||||
return newProtocolError("failed to read COOKIEFILE: %v", err)
|
|
||||||
} else if len(cookie) != authCookieLength {
|
|
||||||
return newProtocolError("invalid cookie file length: %d", len(cookie))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send an AUTHCHALLENGE command, and parse the response.
|
|
||||||
var clientNonce [authNonceLength]byte
|
|
||||||
if _, err := rand.Read(clientNonce[:]); err != nil {
|
|
||||||
return newProtocolError("failed to generate clientNonce: %v", err)
|
|
||||||
}
|
|
||||||
clientNonceStr := hex.EncodeToString(clientNonce[:])
|
|
||||||
resp, err := c.Request("AUTHCHALLENGE %s %s", authMethodSafeCookie, clientNonceStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
splitResp := strings.Split(resp.Reply, " ")
|
|
||||||
if len(splitResp) != 3 {
|
|
||||||
return newProtocolError("invalid AUTHCHALLENGE response")
|
|
||||||
}
|
|
||||||
serverHashStr := strings.TrimPrefix(splitResp[1], "SERVERHASH=")
|
|
||||||
if serverHashStr == splitResp[1] {
|
|
||||||
return newProtocolError("missing SERVERHASH")
|
|
||||||
}
|
|
||||||
serverHash, err := hex.DecodeString(serverHashStr)
|
|
||||||
if err != nil {
|
|
||||||
return newProtocolError("failed to decode ServerHash: %v", err)
|
|
||||||
}
|
|
||||||
if len(serverHash) != authHashLength {
|
|
||||||
return newProtocolError("invalid ServerHash length: %d", len(serverHash))
|
|
||||||
}
|
|
||||||
serverNonceStr := strings.TrimPrefix(splitResp[2], "SERVERNONCE=")
|
|
||||||
if serverNonceStr == splitResp[2] {
|
|
||||||
return newProtocolError("missing SERVERNONCE")
|
|
||||||
}
|
|
||||||
serverNonce, err := hex.DecodeString(serverNonceStr)
|
|
||||||
if err != nil {
|
|
||||||
return newProtocolError("failed to decode ServerNonce: %v", err)
|
|
||||||
}
|
|
||||||
if len(serverNonce) != authNonceLength {
|
|
||||||
return newProtocolError("invalid ServerNonce length: %d", len(serverNonce))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the ServerHash.
|
|
||||||
m := hmac.New(sha256.New, []byte(authServerHashKey))
|
|
||||||
m.Write(cookie)
|
|
||||||
m.Write(clientNonce[:])
|
|
||||||
m.Write(serverNonce)
|
|
||||||
dervServerHash := m.Sum(nil)
|
|
||||||
if !hmac.Equal(serverHash, dervServerHash) {
|
|
||||||
return newProtocolError("invalid ServerHash: mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the ClientHash, and issue the AUTHENTICATE.
|
|
||||||
m = hmac.New(sha256.New, []byte(authClientHashKey))
|
|
||||||
m.Write(cookie)
|
|
||||||
m.Write(clientNonce[:])
|
|
||||||
m.Write(serverNonce)
|
|
||||||
clientHash := m.Sum(nil)
|
|
||||||
clientHashStr := hex.EncodeToString(clientHash)
|
|
||||||
|
|
||||||
_, err = c.Request("%s %s", cmdAuthenticate, clientHashStr)
|
|
||||||
c.isAuthenticated = err == nil
|
|
||||||
return err
|
|
||||||
} else if pi.AuthMethods[authMethodPassword] {
|
|
||||||
// Despite the name HASHEDPASSWORD, the raw password is actually sent.
|
|
||||||
// According to the code, this can either be a QuotedString, or base16
|
|
||||||
// encoded, so go with the later since it's easier to handle.
|
|
||||||
if password == "" {
|
|
||||||
return newProtocolError("password auth needs a password")
|
|
||||||
}
|
|
||||||
passwordStr := hex.EncodeToString([]byte(password))
|
|
||||||
_, err = c.Request("%s %s", cmdAuthenticate, passwordStr)
|
|
||||||
c.isAuthenticated = err == nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return newProtocolError("no supported authentication methods")
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
// cmd_onion.go - various onion service commands: ADD_ONION, DEL_ONION...
|
|
||||||
//
|
|
||||||
// To the extent possible under law, David Stainton and Ivan Markin waived
|
|
||||||
// all copyright and related or neighboring rights to this module of bulb,
|
|
||||||
// using the creative commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/yawning/bulb/utils/pkcs1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnionInfo is the result of the AddOnion command.
|
|
||||||
type OnionInfo struct {
|
|
||||||
OnionID string
|
|
||||||
PrivateKey crypto.PrivateKey
|
|
||||||
|
|
||||||
RawResponse *Response
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnionPrivateKey is a unknown Onion private key (crypto.PublicKey).
|
|
||||||
type OnionPrivateKey struct {
|
|
||||||
KeyType string
|
|
||||||
Key string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnionPortSpec is a Onion VirtPort/Target pair.
|
|
||||||
type OnionPortSpec struct {
|
|
||||||
VirtPort uint16
|
|
||||||
Target string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOnionConfig is a configuration for NewOnion command.
|
|
||||||
type NewOnionConfig struct {
|
|
||||||
PortSpecs []OnionPortSpec
|
|
||||||
PrivateKey crypto.PrivateKey
|
|
||||||
DiscardPK bool
|
|
||||||
Detach bool
|
|
||||||
BasicAuth bool
|
|
||||||
NonAnonymous bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOnion issues an ADD_ONION command using configuration config and
|
|
||||||
// returns the parsed response.
|
|
||||||
func (c *Conn) NewOnion(config *NewOnionConfig) (*OnionInfo, error) {
|
|
||||||
const keyTypeRSA = "RSA1024"
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var portStr string
|
|
||||||
if config.PortSpecs == nil {
|
|
||||||
return nil, newProtocolError("invalid port specification")
|
|
||||||
}
|
|
||||||
for _, v := range config.PortSpecs {
|
|
||||||
portStr += fmt.Sprintf(" Port=%d", v.VirtPort)
|
|
||||||
if v.Target != "" {
|
|
||||||
portStr += "," + v.Target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hsKeyType, hsKeyStr string
|
|
||||||
if config.PrivateKey != nil {
|
|
||||||
switch t := config.PrivateKey.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
rsaPK, _ := config.PrivateKey.(*rsa.PrivateKey)
|
|
||||||
if rsaPK.N.BitLen() != 1024 {
|
|
||||||
return nil, newProtocolError("invalid RSA key size")
|
|
||||||
}
|
|
||||||
pkDER, err := pkcs1.EncodePrivateKeyDER(rsaPK)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newProtocolError("failed to serialize RSA key: %v", err)
|
|
||||||
}
|
|
||||||
hsKeyType = keyTypeRSA
|
|
||||||
hsKeyStr = base64.StdEncoding.EncodeToString(pkDER)
|
|
||||||
case *OnionPrivateKey:
|
|
||||||
genericPK, _ := config.PrivateKey.(*OnionPrivateKey)
|
|
||||||
hsKeyType = genericPK.KeyType
|
|
||||||
hsKeyStr = genericPK.Key
|
|
||||||
default:
|
|
||||||
return nil, newProtocolError("unsupported private key type: %v", t)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hsKeyStr = "BEST"
|
|
||||||
hsKeyType = "NEW"
|
|
||||||
}
|
|
||||||
|
|
||||||
var flags []string
|
|
||||||
var flagsStr string
|
|
||||||
|
|
||||||
if config.DiscardPK {
|
|
||||||
flags = append(flags, "DiscardPK")
|
|
||||||
}
|
|
||||||
if config.Detach {
|
|
||||||
flags = append(flags, "Detach")
|
|
||||||
}
|
|
||||||
if config.BasicAuth {
|
|
||||||
flags = append(flags, "BasicAuth")
|
|
||||||
}
|
|
||||||
if config.NonAnonymous {
|
|
||||||
flags = append(flags, "NonAnonymous")
|
|
||||||
}
|
|
||||||
if flags != nil {
|
|
||||||
flagsStr = " Flags="
|
|
||||||
flagsStr += strings.Join(flags, ",")
|
|
||||||
}
|
|
||||||
request := fmt.Sprintf("ADD_ONION %s:%s%s%s", hsKeyType, hsKeyStr, portStr, flagsStr)
|
|
||||||
resp, err := c.Request(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse out the response.
|
|
||||||
var serviceID string
|
|
||||||
var hsPrivateKey crypto.PrivateKey
|
|
||||||
for _, l := range resp.Data {
|
|
||||||
const (
|
|
||||||
serviceIDPrefix = "ServiceID="
|
|
||||||
privateKeyPrefix = "PrivateKey="
|
|
||||||
)
|
|
||||||
|
|
||||||
if strings.HasPrefix(l, serviceIDPrefix) {
|
|
||||||
serviceID = strings.TrimPrefix(l, serviceIDPrefix)
|
|
||||||
} else if strings.HasPrefix(l, privateKeyPrefix) {
|
|
||||||
if config.DiscardPK || hsKeyStr != "" {
|
|
||||||
return nil, newProtocolError("received an unexpected private key")
|
|
||||||
}
|
|
||||||
hsKeyStr = strings.TrimPrefix(l, privateKeyPrefix)
|
|
||||||
splitKey := strings.SplitN(hsKeyStr, ":", 2)
|
|
||||||
if len(splitKey) != 2 {
|
|
||||||
return nil, newProtocolError("failed to parse private key type")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch splitKey[0] {
|
|
||||||
case keyTypeRSA:
|
|
||||||
keyBlob, err := base64.StdEncoding.DecodeString(splitKey[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, newProtocolError("failed to base64 decode RSA key: %v", err)
|
|
||||||
}
|
|
||||||
hsPrivateKey, _, err = pkcs1.DecodePrivateKeyDER(keyBlob)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newProtocolError("failed to deserialize RSA key: %v", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
hsPrivateKey := new(OnionPrivateKey)
|
|
||||||
hsPrivateKey.KeyType = splitKey[0]
|
|
||||||
hsPrivateKey.Key = splitKey[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if serviceID == "" {
|
|
||||||
// This should *NEVER* happen, since the command succeded, and the spec
|
|
||||||
// guarantees that this will always be present.
|
|
||||||
return nil, newProtocolError("failed to determine service ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
oi := new(OnionInfo)
|
|
||||||
oi.RawResponse = resp
|
|
||||||
oi.OnionID = serviceID
|
|
||||||
oi.PrivateKey = hsPrivateKey
|
|
||||||
|
|
||||||
return oi, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// [DEPRECATED] AddOnion issues an ADD_ONION command and
|
|
||||||
// returns the parsed response.
|
|
||||||
func (c *Conn) AddOnion(ports []OnionPortSpec, key crypto.PrivateKey, oneshot bool) (*OnionInfo, error) {
|
|
||||||
cfg := &NewOnionConfig{}
|
|
||||||
cfg.PortSpecs = ports
|
|
||||||
if key != nil {
|
|
||||||
cfg.PrivateKey = key
|
|
||||||
}
|
|
||||||
cfg.DiscardPK = oneshot
|
|
||||||
return c.NewOnion(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteOnion issues a DEL_ONION command and returns the parsed response.
|
|
||||||
func (c *Conn) DeleteOnion(serviceID string) error {
|
|
||||||
_, err := c.Request("DEL_ONION %s", serviceID)
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
// cmd_protocolinfo.go - PROTOCOLINFO command.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/yawning/bulb/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProtocolInfo is the result of the ProtocolInfo command.
|
|
||||||
type ProtocolInfo struct {
|
|
||||||
AuthMethods map[string]bool
|
|
||||||
CookieFile string
|
|
||||||
TorVersion string
|
|
||||||
|
|
||||||
RawResponse *Response
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProtocolInfo issues a PROTOCOLINFO command and returns the parsed response.
|
|
||||||
func (c *Conn) ProtocolInfo() (*ProtocolInfo, error) {
|
|
||||||
// In the pre-authentication state, only one PROTOCOLINFO command
|
|
||||||
// may be issued. Cache the value returned so that subsequent
|
|
||||||
// calls continue to work.
|
|
||||||
if !c.isAuthenticated && c.cachedPI != nil {
|
|
||||||
return c.cachedPI, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.Request("PROTOCOLINFO")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse out the PIVERSION to make sure it speaks something we understand.
|
|
||||||
if len(resp.Data) < 1 {
|
|
||||||
return nil, newProtocolError("missing PIVERSION")
|
|
||||||
}
|
|
||||||
switch resp.Data[0] {
|
|
||||||
case "1":
|
|
||||||
return nil, newProtocolError("invalid PIVERSION: '%s'", resp.Reply)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse out the rest of the lines.
|
|
||||||
pi := new(ProtocolInfo)
|
|
||||||
pi.RawResponse = resp
|
|
||||||
pi.AuthMethods = make(map[string]bool)
|
|
||||||
for i := 1; i < len(resp.Data); i++ {
|
|
||||||
splitLine := utils.SplitQuoted(resp.Data[i], '"', ' ')
|
|
||||||
switch splitLine[0] {
|
|
||||||
case "AUTH":
|
|
||||||
// Parse an AuthLine detailing how to authenticate.
|
|
||||||
if len(splitLine) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
methods := strings.TrimPrefix(splitLine[1], "METHODS=")
|
|
||||||
if methods == splitLine[1] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, meth := range strings.Split(methods, ",") {
|
|
||||||
pi.AuthMethods[meth] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(splitLine) < 3 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cookiePath := strings.TrimPrefix(splitLine[2], "COOKIEFILE=")
|
|
||||||
if cookiePath == splitLine[2] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pi.CookieFile, _ = strconv.Unquote(cookiePath)
|
|
||||||
case "VERSION":
|
|
||||||
// Parse a VersionLine detailing the Tor version.
|
|
||||||
if len(splitLine) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
torVersion := strings.TrimPrefix(splitLine[1], "Tor=")
|
|
||||||
if torVersion == splitLine[1] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pi.TorVersion, _ = strconv.Unquote(torVersion)
|
|
||||||
default: // MUST ignore unsupported InfoLines.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !c.isAuthenticated {
|
|
||||||
c.cachedPI = pi
|
|
||||||
}
|
|
||||||
return pi, nil
|
|
||||||
}
|
|
|
@ -1,233 +0,0 @@
|
||||||
// conn.go - Controller connection instance.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
// Package bulb is a Go language interface to a Tor control port.
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
gofmt "fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/textproto"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxEventBacklog = 16
|
|
||||||
maxResponseBacklog = 16
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNoAsyncReader is the error returned when the asynchronous event handling
|
|
||||||
// is requested, but the helper go routine has not been started.
|
|
||||||
var ErrNoAsyncReader = errors.New("event requested without an async reader")
|
|
||||||
|
|
||||||
// Conn is a control port connection instance.
|
|
||||||
type Conn struct {
|
|
||||||
conn *textproto.Conn
|
|
||||||
isAuthenticated bool
|
|
||||||
debugLog bool
|
|
||||||
cachedPI *ProtocolInfo
|
|
||||||
|
|
||||||
asyncReaderLock sync.Mutex
|
|
||||||
asyncReaderRunning bool
|
|
||||||
eventChan chan *Response
|
|
||||||
respChan chan *Response
|
|
||||||
closeWg sync.WaitGroup
|
|
||||||
|
|
||||||
rdErrLock sync.Mutex
|
|
||||||
rdErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) setRdErr(err error, force bool) {
|
|
||||||
c.rdErrLock.Lock()
|
|
||||||
defer c.rdErrLock.Unlock()
|
|
||||||
if c.rdErr == nil || force {
|
|
||||||
c.rdErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) getRdErr() error {
|
|
||||||
c.rdErrLock.Lock()
|
|
||||||
defer c.rdErrLock.Unlock()
|
|
||||||
return c.rdErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) isAsyncReaderRunning() bool {
|
|
||||||
c.asyncReaderLock.Lock()
|
|
||||||
defer c.asyncReaderLock.Unlock()
|
|
||||||
return c.asyncReaderRunning
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) asyncReader() {
|
|
||||||
for {
|
|
||||||
resp, err := c.ReadResponse()
|
|
||||||
if err != nil {
|
|
||||||
c.setRdErr(err, false)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if resp.IsAsync() {
|
|
||||||
c.eventChan <- resp
|
|
||||||
} else {
|
|
||||||
c.respChan <- resp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(c.eventChan)
|
|
||||||
close(c.respChan)
|
|
||||||
c.closeWg.Done()
|
|
||||||
|
|
||||||
// In theory, we would lock and set asyncReaderRunning to false here, but
|
|
||||||
// once it's started, the only way it returns is if there is a catastrophic
|
|
||||||
// failure, or a graceful shutdown. Changing this will require redoing how
|
|
||||||
// Close() works.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug enables/disables debug logging of control port chatter.
|
|
||||||
func (c *Conn) Debug(enable bool) {
|
|
||||||
c.debugLog = enable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection.
|
|
||||||
func (c *Conn) Close() error {
|
|
||||||
c.asyncReaderLock.Lock()
|
|
||||||
defer c.asyncReaderLock.Unlock()
|
|
||||||
|
|
||||||
err := c.conn.Close()
|
|
||||||
if err != nil && c.asyncReaderRunning {
|
|
||||||
c.closeWg.Wait()
|
|
||||||
}
|
|
||||||
c.setRdErr(io.ErrClosedPipe, true)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartAsyncReader starts the asynchronous reader go routine that allows
|
|
||||||
// asynchronous events to be handled. It must not be called simultaniously
|
|
||||||
// with Read, Request, or ReadResponse or undefined behavior will occur.
|
|
||||||
func (c *Conn) StartAsyncReader() {
|
|
||||||
c.asyncReaderLock.Lock()
|
|
||||||
defer c.asyncReaderLock.Unlock()
|
|
||||||
if c.asyncReaderRunning {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate the channels and kick off the read worker.
|
|
||||||
c.eventChan = make(chan *Response, maxEventBacklog)
|
|
||||||
c.respChan = make(chan *Response, maxResponseBacklog)
|
|
||||||
c.closeWg.Add(1)
|
|
||||||
go c.asyncReader()
|
|
||||||
c.asyncReaderRunning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextEvent returns the next asynchronous event received, blocking if
|
|
||||||
// neccecary. In order to enable asynchronous event handling, StartAsyncReader
|
|
||||||
// must be called first.
|
|
||||||
func (c *Conn) NextEvent() (*Response, error) {
|
|
||||||
if err := c.getRdErr(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !c.isAsyncReaderRunning() {
|
|
||||||
return nil, ErrNoAsyncReader
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, ok := <-c.eventChan
|
|
||||||
if resp != nil {
|
|
||||||
return resp, nil
|
|
||||||
} else if !ok {
|
|
||||||
return nil, io.ErrClosedPipe
|
|
||||||
}
|
|
||||||
panic("BUG: NextEvent() returned a nil response and error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request sends a raw control port request and returns the response.
|
|
||||||
// If the async. reader is not currently running, events received while waiting
|
|
||||||
// for the response will be silently dropped. Calling Request simultaniously
|
|
||||||
// with StartAsyncReader, Read, Write, or ReadResponse will lead to undefined
|
|
||||||
// behavior.
|
|
||||||
func (c *Conn) Request(fmt string, args ...interface{}) (*Response, error) {
|
|
||||||
if err := c.getRdErr(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
asyncResp := c.isAsyncReaderRunning()
|
|
||||||
|
|
||||||
if c.debugLog {
|
|
||||||
log.Printf("C: %s", gofmt.Sprintf(fmt, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := c.conn.Cmd(fmt, args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.conn.StartResponse(id)
|
|
||||||
defer c.conn.EndResponse(id)
|
|
||||||
var resp *Response
|
|
||||||
if asyncResp {
|
|
||||||
var ok bool
|
|
||||||
resp, ok = <-c.respChan
|
|
||||||
if resp == nil && !ok {
|
|
||||||
return nil, io.ErrClosedPipe
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Event handing requires the asyncReader() goroutine, try to get a
|
|
||||||
// response, while silently swallowing events.
|
|
||||||
for resp == nil || resp.IsAsync() {
|
|
||||||
resp, err = c.ReadResponse()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
panic("BUG: Request() returned a nil response and error")
|
|
||||||
}
|
|
||||||
if resp.IsOk() {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
return resp, resp.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads directly from the control port connection. Mixing this call
|
|
||||||
// with Request, ReadResponse, or asynchronous events will lead to undefined
|
|
||||||
// behavior.
|
|
||||||
func (c *Conn) Read(p []byte) (int, error) {
|
|
||||||
return c.conn.R.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes directly from the control port connection. Mixing this call
|
|
||||||
// with Request will lead to undefined behavior.
|
|
||||||
func (c *Conn) Write(p []byte) (int, error) {
|
|
||||||
n, err := c.conn.W.Write(p)
|
|
||||||
if err == nil {
|
|
||||||
// If the write succeeds, but the flush fails, n will be incorrect...
|
|
||||||
return n, c.conn.W.Flush()
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial connects to a given network/address and returns a new Conn for the
|
|
||||||
// connection.
|
|
||||||
func Dial(network, addr string) (*Conn, error) {
|
|
||||||
c, err := net.Dial(network, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewConn(c), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConn returns a new Conn using c for I/O.
|
|
||||||
func NewConn(c io.ReadWriteCloser) *Conn {
|
|
||||||
conn := new(Conn)
|
|
||||||
conn.conn = textproto.NewConn(c)
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func newProtocolError(fmt string, args ...interface{}) textproto.ProtocolError {
|
|
||||||
return textproto.ProtocolError(gofmt.Sprintf(fmt, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ io.ReadWriteCloser = (*Conn)(nil)
|
|
|
@ -1,54 +0,0 @@
|
||||||
// dialer.go - Tor backed proxy.Dialer.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/proxy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Dialer returns a proxy.Dialer for the given Tor instance.
|
|
||||||
func (c *Conn) Dialer(auth *proxy.Auth) (proxy.Dialer, error) {
|
|
||||||
const (
|
|
||||||
cmdGetInfo = "GETINFO"
|
|
||||||
socksListeners = "net/listeners/socks"
|
|
||||||
unixPrefix = "unix:"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Query for the SOCKS listeners via a GETINFO request.
|
|
||||||
resp, err := c.Request("%s %s", cmdGetInfo, socksListeners)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Data) != 1 {
|
|
||||||
return nil, newProtocolError("no SOCKS listeners configured")
|
|
||||||
}
|
|
||||||
splitResp := strings.Split(resp.Data[0], " ")
|
|
||||||
if len(splitResp) < 1 {
|
|
||||||
return nil, newProtocolError("no SOCKS listeners configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first listener will have a "net/listeners/socks=" prefix, and all
|
|
||||||
// entries are QuotedStrings.
|
|
||||||
laddrStr := strings.TrimPrefix(splitResp[0], socksListeners+"=")
|
|
||||||
if laddrStr == splitResp[0] {
|
|
||||||
return nil, newProtocolError("failed to parse SOCKS listener")
|
|
||||||
}
|
|
||||||
laddrStr, _ = strconv.Unquote(laddrStr)
|
|
||||||
|
|
||||||
// Construct the proxyDialer.
|
|
||||||
if strings.HasPrefix(laddrStr, unixPrefix) {
|
|
||||||
unixPath := strings.TrimPrefix(laddrStr, unixPrefix)
|
|
||||||
return proxy.SOCKS5("unix", unixPath, auth, proxy.Direct)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxy.SOCKS5("tcp", laddrStr, auth, proxy.Direct)
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
// listener.go - Tor backed net.Listener.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel and Ivan Markin
|
|
||||||
// waived all copyright and related or neighboring rights to bulb, using
|
|
||||||
// the creative commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type onionAddr struct {
|
|
||||||
info *OnionInfo
|
|
||||||
port uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *onionAddr) Network() string {
|
|
||||||
return "tcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *onionAddr) String() string {
|
|
||||||
return fmt.Sprintf("%s.onion:%d", a.info.OnionID, a.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
type onionListener struct {
|
|
||||||
addr *onionAddr
|
|
||||||
ctrlConn *Conn
|
|
||||||
listener net.Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *onionListener) Accept() (net.Conn, error) {
|
|
||||||
return l.listener.Accept()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *onionListener) Close() (err error) {
|
|
||||||
if err = l.listener.Close(); err == nil {
|
|
||||||
// Only delete the onion once.
|
|
||||||
err = l.ctrlConn.DeleteOnion(l.addr.info.OnionID)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *onionListener) Addr() net.Addr {
|
|
||||||
return l.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewListener returns a net.Listener backed by an Onion Service using configuration
|
|
||||||
// config, optionally having Tor generate an ephemeral private key (config is nil or
|
|
||||||
// config.PrivateKey is nil).
|
|
||||||
// All of virtual ports specified in vports will be mapped to the port to which
|
|
||||||
// the underlying TCP listener binded. PortSpecs in config will be ignored since
|
|
||||||
// there is only one mapping for a vports set is possible.
|
|
||||||
func (c *Conn) NewListener(config *NewOnionConfig, vports ...uint16) (net.Listener, error) {
|
|
||||||
var cfg NewOnionConfig
|
|
||||||
if config == nil {
|
|
||||||
cfg = NewOnionConfig{
|
|
||||||
DiscardPK: true,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cfg = *config
|
|
||||||
}
|
|
||||||
|
|
||||||
const loopbackAddr = "127.0.0.1:0"
|
|
||||||
|
|
||||||
// Listen on the loopback interface.
|
|
||||||
tcpListener, err := net.Listen("tcp4", loopbackAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tAddr, ok := tcpListener.Addr().(*net.TCPAddr)
|
|
||||||
if !ok {
|
|
||||||
tcpListener.Close()
|
|
||||||
return nil, newProtocolError("failed to extract local port")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(vports) < 1 {
|
|
||||||
return nil, newProtocolError("no virual ports specified")
|
|
||||||
}
|
|
||||||
targetPortStr := strconv.FormatUint((uint64)(tAddr.Port), 10)
|
|
||||||
var portSpecs []OnionPortSpec
|
|
||||||
for _, vport := range vports {
|
|
||||||
portSpecs = append(portSpecs, OnionPortSpec{
|
|
||||||
VirtPort: vport,
|
|
||||||
Target: targetPortStr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
cfg.PortSpecs = portSpecs
|
|
||||||
// Create the onion.
|
|
||||||
oi, err := c.NewOnion(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
tcpListener.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oa := &onionAddr{info: oi, port: vports[0]}
|
|
||||||
ol := &onionListener{addr: oa, ctrlConn: c, listener: tcpListener}
|
|
||||||
|
|
||||||
return ol, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// [DEPRECATED] Listener returns a net.Listener backed by an Onion Service.
|
|
||||||
func (c *Conn) Listener(port uint16, key crypto.PrivateKey) (net.Listener, error) {
|
|
||||||
cfg := &NewOnionConfig{}
|
|
||||||
if key != nil {
|
|
||||||
cfg.PrivateKey = key
|
|
||||||
cfg.DiscardPK = false
|
|
||||||
} else {
|
|
||||||
cfg.DiscardPK = true
|
|
||||||
}
|
|
||||||
return c.NewListener(cfg, port)
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
// response.go - Generic response handler
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/textproto"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Response is a response to a control port command, or an asyncrhonous event.
|
|
||||||
type Response struct {
|
|
||||||
// Err is the status code and string representation associated with a
|
|
||||||
// response. Responses that have completed successfully will also have
|
|
||||||
// Err set to indicate such.
|
|
||||||
Err *textproto.Error
|
|
||||||
|
|
||||||
// Reply is the text on the EndReplyLine of the response.
|
|
||||||
Reply string
|
|
||||||
|
|
||||||
// Data is the MidReplyLines/DataReplyLines of the response. Dot encoded
|
|
||||||
// data is "decoded" and presented as a single string (terminal ".CRLF"
|
|
||||||
// removed, all intervening CRs stripped).
|
|
||||||
Data []string
|
|
||||||
|
|
||||||
// RawLines is all of the lines of a response, without CRLFs.
|
|
||||||
RawLines []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsOk returns true if the response status code indicates success or
|
|
||||||
// an asynchronous event.
|
|
||||||
func (r *Response) IsOk() bool {
|
|
||||||
switch r.Err.Code {
|
|
||||||
case StatusOk, StatusOkUnneccecary, StatusAsyncEvent:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAsync returns true if the response is an asyncrhonous event.
|
|
||||||
func (r *Response) IsAsync() bool {
|
|
||||||
return r.Err.Code == StatusAsyncEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadResponse returns the next response object. Calling this
|
|
||||||
// simultaniously with Read, Request, or StartAsyncReader will lead to
|
|
||||||
// undefined behavior
|
|
||||||
func (c *Conn) ReadResponse() (*Response, error) {
|
|
||||||
var resp *Response
|
|
||||||
var statusCode int
|
|
||||||
for {
|
|
||||||
line, err := c.conn.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c.debugLog {
|
|
||||||
log.Printf("S: %s", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the line that was just read.
|
|
||||||
if len(line) < 4 {
|
|
||||||
return nil, newProtocolError("truncated response: '%s'", line)
|
|
||||||
}
|
|
||||||
if code, err := strconv.Atoi(line[0:3]); err != nil {
|
|
||||||
return nil, newProtocolError("invalid status code: '%s'", line[0:3])
|
|
||||||
} else if code < 100 {
|
|
||||||
return nil, newProtocolError("invalid status code: '%s'", line[0:3])
|
|
||||||
} else if resp == nil {
|
|
||||||
resp = new(Response)
|
|
||||||
statusCode = code
|
|
||||||
} else if code != statusCode {
|
|
||||||
// The status code should stay fixed for all lines of the
|
|
||||||
// response, since events can't be interleaved with response
|
|
||||||
// lines.
|
|
||||||
return nil, newProtocolError("status code changed: %03d != %03d", code, statusCode)
|
|
||||||
}
|
|
||||||
if resp.RawLines == nil {
|
|
||||||
resp.RawLines = make([]string, 0, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if line[3] == ' ' {
|
|
||||||
// Final line in the response.
|
|
||||||
resp.Reply = line[4:]
|
|
||||||
resp.Err = statusCodeToError(statusCode, resp.Reply)
|
|
||||||
resp.RawLines = append(resp.RawLines, line)
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Data == nil {
|
|
||||||
resp.Data = make([]string, 0, 1)
|
|
||||||
}
|
|
||||||
switch line[3] {
|
|
||||||
case '-':
|
|
||||||
// Continuation, keep reading.
|
|
||||||
resp.Data = append(resp.Data, line[4:])
|
|
||||||
resp.RawLines = append(resp.RawLines, line)
|
|
||||||
case '+':
|
|
||||||
// A "dot-encoded" payload follows.
|
|
||||||
resp.Data = append(resp.Data, line[4:])
|
|
||||||
resp.RawLines = append(resp.RawLines, line)
|
|
||||||
dotBody, err := c.conn.ReadDotBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c.debugLog {
|
|
||||||
log.Printf("S: [dot encoded data]")
|
|
||||||
}
|
|
||||||
resp.Data = append(resp.Data, strings.TrimRight(string(dotBody), "\n\r"))
|
|
||||||
dotLines := strings.Split(string(dotBody), "\n")
|
|
||||||
for _, dotLine := range dotLines[:len(dotLines)-1] {
|
|
||||||
resp.RawLines = append(resp.RawLines, dotLine)
|
|
||||||
}
|
|
||||||
resp.RawLines = append(resp.RawLines, ".")
|
|
||||||
default:
|
|
||||||
return nil, newProtocolError("invalid separator: '%c'", line[3])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
// status.go - Status codes.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
package bulb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"net/textproto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The various control port StatusCode constants.
|
|
||||||
const (
|
|
||||||
StatusOk = 250
|
|
||||||
StatusOkUnneccecary = 251
|
|
||||||
|
|
||||||
StatusErrResourceExhausted = 451
|
|
||||||
StatusErrSyntaxError = 500
|
|
||||||
StatusErrUnrecognizedCmd = 510
|
|
||||||
StatusErrUnimplementedCmd = 511
|
|
||||||
StatusErrSyntaxErrorArg = 512
|
|
||||||
StatusErrUnrecognizedCmdArg = 513
|
|
||||||
StatusErrAuthenticationRequired = 514
|
|
||||||
StatusErrBadAuthentication = 515
|
|
||||||
StatusErrUnspecifiedTorError = 550
|
|
||||||
StatusErrInternalError = 551
|
|
||||||
StatusErrUnrecognizedEntity = 552
|
|
||||||
StatusErrInvalidConfigValue = 553
|
|
||||||
StatusErrInvalidDescriptor = 554
|
|
||||||
StatusErrUnmanagedEntity = 555
|
|
||||||
|
|
||||||
StatusAsyncEvent = 650
|
|
||||||
)
|
|
||||||
|
|
||||||
var statusCodeStringMap = map[int]string{
|
|
||||||
StatusOk: "OK",
|
|
||||||
StatusOkUnneccecary: "Operation was unnecessary",
|
|
||||||
|
|
||||||
StatusErrResourceExhausted: "Resource exhausted",
|
|
||||||
StatusErrSyntaxError: "Syntax error: protocol",
|
|
||||||
StatusErrUnrecognizedCmd: "Unrecognized command",
|
|
||||||
StatusErrUnimplementedCmd: "Unimplemented command",
|
|
||||||
StatusErrSyntaxErrorArg: "Syntax error in command argument",
|
|
||||||
StatusErrUnrecognizedCmdArg: "Unrecognized command argument",
|
|
||||||
StatusErrAuthenticationRequired: "Authentication required",
|
|
||||||
StatusErrBadAuthentication: "Bad authentication",
|
|
||||||
StatusErrUnspecifiedTorError: "Unspecified Tor error",
|
|
||||||
StatusErrInternalError: "Internal error",
|
|
||||||
StatusErrUnrecognizedEntity: "Unrecognized entity",
|
|
||||||
StatusErrInvalidConfigValue: "Invalid configuration value",
|
|
||||||
StatusErrInvalidDescriptor: "Invalid descriptor",
|
|
||||||
StatusErrUnmanagedEntity: "Unmanaged entity",
|
|
||||||
|
|
||||||
StatusAsyncEvent: "Asynchronous event notification",
|
|
||||||
}
|
|
||||||
|
|
||||||
func statusCodeToError(code int, reply string) *textproto.Error {
|
|
||||||
err := new(textproto.Error)
|
|
||||||
err.Code = code
|
|
||||||
if msg, ok := statusCodeStringMap[code]; ok {
|
|
||||||
trimmedReply := strings.TrimSpace(strings.TrimPrefix(reply, msg))
|
|
||||||
err.Msg = fmt.Sprintf("%s: %s", msg, trimmedReply)
|
|
||||||
} else {
|
|
||||||
err.Msg = fmt.Sprintf("Unknown status code (%03d): %s", code, reply)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
//
|
|
||||||
// rsa.go - PKCS#1 RSA key related helpers.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel has waived all copyright and
|
|
||||||
// related or neighboring rights to bulb, using the creative commons
|
|
||||||
// "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
// Package pkcs1 implements PKCS#1 RSA key marshalling/unmarshalling,
|
|
||||||
// compatibile with Tor's usage.
|
|
||||||
package pkcs1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base32"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pkcs1RSAPrivKey struct {
|
|
||||||
Version int // version
|
|
||||||
N *big.Int // modulus
|
|
||||||
E int // publicExponent
|
|
||||||
D *big.Int // privateExponent
|
|
||||||
P *big.Int // prime1
|
|
||||||
Q *big.Int // prime2
|
|
||||||
Dp *big.Int // exponent1: d mod (p-1)
|
|
||||||
Dq *big.Int // exponent2: d mod (q-1)
|
|
||||||
Qinv *big.Int // coefficient: (inverse of q) mod p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodePrivateKeyDER returns the PKCS#1 DER encoding of a rsa.PrivateKey.
|
|
||||||
func EncodePrivateKeyDER(sk *rsa.PrivateKey) ([]byte, error) {
|
|
||||||
// The crypto.RSA structure has a slightly different layout than PKCS#1
|
|
||||||
// private keys, so directly marshaling does not work. Pull out the values
|
|
||||||
// into a strucuture with the correct layout and marshal.
|
|
||||||
sk.Precompute() // Ensure that the structure is fully populated.
|
|
||||||
k := pkcs1RSAPrivKey{
|
|
||||||
Version: 0,
|
|
||||||
N: sk.N,
|
|
||||||
E: sk.E,
|
|
||||||
D: sk.D,
|
|
||||||
P: sk.Primes[0],
|
|
||||||
Q: sk.Primes[1],
|
|
||||||
Dp: sk.Precomputed.Dp,
|
|
||||||
Dq: sk.Precomputed.Dq,
|
|
||||||
Qinv: sk.Precomputed.Qinv,
|
|
||||||
}
|
|
||||||
return asn1.Marshal(k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodePrivateKeyDER returns the rsa.PrivateKey decoding of a PKCS#1 DER blob.
|
|
||||||
func DecodePrivateKeyDER(b []byte) (*rsa.PrivateKey, []byte, error) {
|
|
||||||
var k pkcs1RSAPrivKey
|
|
||||||
rest, err := asn1.Unmarshal(b, &k)
|
|
||||||
if err == nil {
|
|
||||||
sk := &rsa.PrivateKey{}
|
|
||||||
sk.Primes = make([]*big.Int, 2)
|
|
||||||
sk.N = k.N
|
|
||||||
sk.E = k.E
|
|
||||||
sk.D = k.D
|
|
||||||
sk.Primes[0] = k.P
|
|
||||||
sk.Primes[1] = k.Q
|
|
||||||
|
|
||||||
// Ignore the precomputed values and just rederive them.
|
|
||||||
sk.Precompute()
|
|
||||||
return sk, rest, nil
|
|
||||||
}
|
|
||||||
return nil, rest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodePublicKeyDER returns the PKCS#1 DER encoding of a rsa.PublicKey.
|
|
||||||
func EncodePublicKeyDER(pk *rsa.PublicKey) ([]byte, error) {
|
|
||||||
// The crypto.RSA structure is exactly the same as the PCKS#1 public keys,
|
|
||||||
// when the encoding/asn.1 marshaller is done with it.
|
|
||||||
//
|
|
||||||
// DER encoding of (SEQUENCE | INTEGER(n) | INTEGER(e))
|
|
||||||
return asn1.Marshal(*pk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodePublicKeyDER returns the rsa.PublicKey decoding of a PKCS#1 DER blob.
|
|
||||||
func DecodePublicKeyDER(b []byte) (*rsa.PublicKey, []byte, error) {
|
|
||||||
pk := &rsa.PublicKey{}
|
|
||||||
rest, err := asn1.Unmarshal(b, pk)
|
|
||||||
return pk, rest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnionAddr returns the Tor Onion Service address corresponding to a given
|
|
||||||
// rsa.PublicKey.
|
|
||||||
func OnionAddr(pk *rsa.PublicKey) (string, error) {
|
|
||||||
der, err := EncodePublicKeyDER(pk)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
h := sha1.Sum(der)
|
|
||||||
hb32 := base32.StdEncoding.EncodeToString(h[:10])
|
|
||||||
|
|
||||||
return strings.ToLower(hb32), nil
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
// utils.go - A grab bag of useful utilitiy functions.
|
|
||||||
//
|
|
||||||
// To the extent possible under law, Yawning Angel waived all copyright
|
|
||||||
// and related or neighboring rights to bulb, using the creative
|
|
||||||
// commons "cc0" public domain dedication. See LICENSE or
|
|
||||||
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
|
||||||
|
|
||||||
// Package utils implements useful utilities for dealing with Tor and it's
|
|
||||||
// control port.
|
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SplitQuoted splits s by sep if it is found outside substring
|
|
||||||
// quoted by quote.
|
|
||||||
func SplitQuoted(s string, quote, sep rune) (splitted []string) {
|
|
||||||
quoteFlag := false
|
|
||||||
NewSubstring:
|
|
||||||
for i, c := range s {
|
|
||||||
if c == quote {
|
|
||||||
quoteFlag = !quoteFlag
|
|
||||||
}
|
|
||||||
if c == sep && !quoteFlag {
|
|
||||||
splitted = append(splitted, s[:i])
|
|
||||||
s = s[i+1:]
|
|
||||||
goto NewSubstring
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return append(splitted, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseControlPortString parses a string representation of a control port
|
|
||||||
// address into a network/address string pair suitable for use with "dial".
|
|
||||||
//
|
|
||||||
// Valid string representations are:
|
|
||||||
// * tcp://address:port
|
|
||||||
// * unix://path
|
|
||||||
// * port (Translates to tcp://127.0.0.1:port)
|
|
||||||
func ParseControlPortString(raw string) (network, addr string, err error) {
|
|
||||||
// Try parsing it as a naked port.
|
|
||||||
if _, err = strconv.ParseUint(raw, 10, 16); err == nil {
|
|
||||||
raw = "tcp://127.0.0.1:" + raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ok, parse/validate the URI.
|
|
||||||
uri, err := url.Parse(raw)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if uri.Opaque != "" || uri.RawQuery != "" || uri.Fragment != "" {
|
|
||||||
return "", "", net.InvalidAddrError("uri has Opaque/Query/Fragment")
|
|
||||||
}
|
|
||||||
switch uri.Scheme {
|
|
||||||
case "tcp":
|
|
||||||
if uri.Path != "" {
|
|
||||||
return "", "", net.InvalidAddrError("tcp uri has a path")
|
|
||||||
}
|
|
||||||
tcpAddr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if tcpAddr.Port == 0 {
|
|
||||||
return "", "", net.InvalidAddrError("tcp uri is missing a port")
|
|
||||||
}
|
|
||||||
return uri.Scheme, uri.Host, nil
|
|
||||||
case "unix":
|
|
||||||
if uri.Host != "" {
|
|
||||||
return "", "", net.InvalidAddrError("unix uri has a host")
|
|
||||||
}
|
|
||||||
_, err := net.ResolveUnixAddr(uri.Scheme, uri.Path)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return uri.Scheme, uri.Path, nil
|
|
||||||
}
|
|
||||||
return "", "", net.InvalidAddrError("unknown scheme: " + uri.Scheme)
|
|
||||||
}
|
|
Loading…
Reference in New Issue