Commit 37f90cd1 authored by Steven Allen's avatar Steven Allen

initial commit

parents
os:
- linux
sudo: false
language: go
go:
- 1.10.0
install:
- make deps
script:
- bash <(curl -s https://raw.githubusercontent.com/ipfs/ci-helpers/master/travis-ci/run-standard-tests.sh)
cache:
directories:
- $GOPATH/src/gx
notifications:
email: false
export IPFS_API ?= v04x.ipfs.io
gx:
go get -u github.com/whyrusleeping/gx
go get -u github.com/whyrusleeping/gx-go
deps: gx
gx --verbose install --global
gx-go rewrite
# go-ipns
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai)
[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/)
[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs)
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
[![GoDoc](https://godoc.org/github.com/ipfs/go-datastore?status.svg)](https://godoc.org/github.com/ipfs/go-ipns)
> ipns record definitions
This package contains all of components necessary to create, understand, and
validate IPNS records.
## Documentation
https://godoc.org/github.com/ipfs/go-ipns
## Contribute
Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/go-ipns/issues)!
This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
### Want to hack on IPFS?
[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/contributing.md)
## License
MIT
coverage:
range: "50...100"
comment: off
package ipns
import (
"errors"
)
// ErrExpiredRecord should be returned when an ipns record is
// invalid due to being too old
var ErrExpiredRecord = errors.New("expired record")
// ErrUnrecognizedValidity is returned when an IpnsRecord has an
// unknown validity type.
var ErrUnrecognizedValidity = errors.New("unrecognized validity type")
// ErrInvalidPath should be returned when an ipns record path
// is not in a valid format
var ErrInvalidPath = errors.New("record path invalid")
// ErrSignature should be returned when an ipns record fails
// signature verification
var ErrSignature = errors.New("record signature verification failed")
// ErrKeyFormat should be returned when an ipns record key is
// incorrectly formatted (not a peer ID)
var ErrKeyFormat = errors.New("record key could not be parsed into peer ID")
// ErrPublicKeyNotFound should be returned when the public key
// corresponding to the ipns record path cannot be retrieved
// from the peer store
var ErrPublicKeyNotFound = errors.New("public key not found in peer store")
// ErrPublicKeyMismatch should be returned when the public key embedded in the
// record doesn't match the expected public key.
var ErrPublicKeyMismatch = errors.New("public key in record did not match expected pubkey")
// ErrBadRecord should be returned when an ipns record cannot be unmarshalled
var ErrBadRecord = errors.New("record could not be unmarshalled")
package ipns
import (
"bytes"
"fmt"
"time"
proto "github.com/gogo/protobuf/proto"
u "github.com/ipfs/go-ipfs-util"
ic "github.com/libp2p/go-libp2p-crypto"
peer "github.com/libp2p/go-libp2p-peer"
pb "github.com/ipfs/go-ipns/pb"
)
// Create creates a new IPNS entry and signs it with the given private key.
//
// This function does not embed the public key. If you want to do that, use
// `EmbedPublicKey`.
func Create(sk ic.PrivKey, val []byte, seq uint64, eol time.Time) (*pb.IpnsEntry, error) {
entry := new(pb.IpnsEntry)
entry.Value = val
typ := pb.IpnsEntry_EOL
entry.ValidityType = &typ
entry.Sequence = proto.Uint64(seq)
entry.Validity = []byte(u.FormatRFC3339(eol))
sig, err := sk.Sign(ipnsEntryDataForSig(entry))
if err != nil {
return nil, err
}
entry.Signature = sig
return entry, nil
}
// Validates validates the given IPNS entry against the given public key.
func Validate(pk ic.PubKey, entry *pb.IpnsEntry) error {
// Check the ipns record signature with the public key
if ok, err := pk.Verify(ipnsEntryDataForSig(entry), entry.GetSignature()); err != nil || !ok {
return ErrSignature
}
// Check that record has not expired
switch entry.GetValidityType() {
case pb.IpnsEntry_EOL:
t, err := u.ParseRFC3339(string(entry.GetValidity()))
if err != nil {
return err
}
if time.Now().After(t) {
return ErrExpiredRecord
}
default:
return ErrUnrecognizedValidity
}
return nil
}
// EmbedPublicKey embeds the given public key in the given ipns entry. While not
// strictly required, some nodes (e.g., DHT servers) may reject IPNS entries
// that don't embed their public keys as they may not be able to validate them
// efficiently.
func EmbedPublicKey(pk ic.PubKey, entry *pb.IpnsEntry) error {
id, err := peer.IDFromPublicKey(pk)
if err != nil {
return err
}
extraced, err := id.ExtractPublicKey()
if err != nil {
return err
}
if extraced != nil {
return nil
}
pkBytes, err := pk.Bytes()
if err != nil {
return err
}
entry.PubKey = pkBytes
return nil
}
// ExtractPublicKey extracts a public key matching `pid` from the IPNS record,
// if possible.
//
// This function returns (nil, nil) when no public key can be extracted and
// nothing is malformed.
func ExtractPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
if entry.PubKey != nil {
pk, err := ic.UnmarshalPublicKey(entry.PubKey)
if err != nil {
return nil, fmt.Errorf("unmarshaling pubkey in record: %s", err)
}
expPid, err := peer.IDFromPublicKey(pk)
if err != nil {
return nil, fmt.Errorf("could not regenerate peerID from pubkey: %s", err)
}
if pid != expPid {
return nil, ErrPublicKeyMismatch
}
return pk, nil
}
return pid.ExtractPublicKey()
}
// Compare compares two IPNS entries. It returns:
//
// * -1 if a is older than b
// * 0 if a and b cannot be ordered (this doesn't mean that they are equal)
// * +1 if a is newer than b
//
// It returns an error when either a or b are malformed.
//
// NOTE: It *does not* validate the records, the caller is responsible for calling
// `Validate` first.
//
// NOTE: If a and b cannot be ordered by this function, you can determine their
// order by comparing their serialized byte representations (using
// `bytes.Compare`). You must do this if you are implementing a libp2p record
// validator (or you can just use the one provided for you by this package).
func Compare(a, b *pb.IpnsEntry) (int, error) {
as := a.GetSequence()
bs := b.GetSequence()
if as > bs {
return 1, nil
} else if as < bs {
return -1, nil
}
at, err := u.ParseRFC3339(string(a.GetValidity()))
if err != nil {
return 0, err
}
bt, err := u.ParseRFC3339(string(b.GetValidity()))
if err != nil {
return 0, err
}
if at.After(bt) {
return 1, nil
} else if bt.After(at) {
return -1, nil
}
return 0, nil
}
func ipnsEntryDataForSig(e *pb.IpnsEntry) []byte {
return bytes.Join([][]byte{
e.Value,
e.Validity,
[]byte(fmt.Sprint(e.GetValidityType())),
},
[]byte{})
}
include mk/header.mk
PB_$(d) = $(wildcard $(d)/*.proto)
TGTS_$(d) = $(PB_$(d):.proto=.pb.go)
#DEPS_GO += $(TGTS_$(d))
include mk/footer.mk
// Code generated by protoc-gen-gogo.
// source: pb/ipns.proto
// DO NOT EDIT!
/*
Package ipns_pb is a generated protocol buffer package.
It is generated from these files:
pb/ipns.proto
It has these top-level messages:
IpnsEntry
*/
package ipns_pb
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type IpnsEntry_ValidityType int32
const (
// setting an EOL says "this record is valid until..."
IpnsEntry_EOL IpnsEntry_ValidityType = 0
)
var IpnsEntry_ValidityType_name = map[int32]string{
0: "EOL",
}
var IpnsEntry_ValidityType_value = map[string]int32{
"EOL": 0,
}
func (x IpnsEntry_ValidityType) Enum() *IpnsEntry_ValidityType {
p := new(IpnsEntry_ValidityType)
*p = x
return p
}
func (x IpnsEntry_ValidityType) String() string {
return proto.EnumName(IpnsEntry_ValidityType_name, int32(x))
}
func (x *IpnsEntry_ValidityType) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(IpnsEntry_ValidityType_value, data, "IpnsEntry_ValidityType")
if err != nil {
return err
}
*x = IpnsEntry_ValidityType(value)
return nil
}
type IpnsEntry struct {
Value []byte `protobuf:"bytes,1,req,name=value" json:"value,omitempty"`
Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"`
ValidityType *IpnsEntry_ValidityType `protobuf:"varint,3,opt,name=validityType,enum=ipns.pb.IpnsEntry_ValidityType" json:"validityType,omitempty"`
Validity []byte `protobuf:"bytes,4,opt,name=validity" json:"validity,omitempty"`
Sequence *uint64 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"`
Ttl *uint64 `protobuf:"varint,6,opt,name=ttl" json:"ttl,omitempty"`
// in order for nodes to properly validate a record upon receipt, they need the public
// key associated with it. For old RSA keys, its easiest if we just send this as part of
// the record itself. For newer ed25519 keys, the public key can be embedded in the
// peerID, making this field unnecessary.
PubKey []byte `protobuf:"bytes,7,opt,name=pubKey" json:"pubKey,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *IpnsEntry) Reset() { *m = IpnsEntry{} }
func (m *IpnsEntry) String() string { return proto.CompactTextString(m) }
func (*IpnsEntry) ProtoMessage() {}
func (m *IpnsEntry) GetValue() []byte {
if m != nil {
return m.Value
}
return nil
}
func (m *IpnsEntry) GetSignature() []byte {
if m != nil {
return m.Signature
}
return nil
}
func (m *IpnsEntry) GetValidityType() IpnsEntry_ValidityType {
if m != nil && m.ValidityType != nil {
return *m.ValidityType
}
return IpnsEntry_EOL
}
func (m *IpnsEntry) GetValidity() []byte {
if m != nil {
return m.Validity
}
return nil
}
func (m *IpnsEntry) GetSequence() uint64 {
if m != nil && m.Sequence != nil {
return *m.Sequence
}
return 0
}
func (m *IpnsEntry) GetTtl() uint64 {
if m != nil && m.Ttl != nil {
return *m.Ttl
}
return 0
}
func (m *IpnsEntry) GetPubKey() []byte {
if m != nil {
return m.PubKey
}
return nil
}
func init() {
proto.RegisterType((*IpnsEntry)(nil), "ipns.pb.IpnsEntry")
proto.RegisterEnum("ipns.pb.IpnsEntry_ValidityType", IpnsEntry_ValidityType_name, IpnsEntry_ValidityType_value)
}
package ipns.pb;
message IpnsEntry {
enum ValidityType {
// setting an EOL says "this record is valid until..."
EOL = 0;
}
required bytes value = 1;
required bytes signature = 2;
optional ValidityType validityType = 3;
optional bytes validity = 4;
optional uint64 sequence = 5;
optional uint64 ttl = 6;
// in order for nodes to properly validate a record upon receipt, they need the public
// key associated with it. For old RSA keys, its easiest if we just send this as part of
// the record itself. For newer ed25519 keys, the public key can be embedded in the
// peerID, making this field unnecessary.
optional bytes pubKey = 7;
}
package ipns
import (
"bytes"
"errors"
proto "github.com/gogo/protobuf/proto"
logging "github.com/ipfs/go-log"
ic "github.com/libp2p/go-libp2p-crypto"
peer "github.com/libp2p/go-libp2p-peer"
pstore "github.com/libp2p/go-libp2p-peerstore"
record "github.com/libp2p/go-libp2p-record"
pb "github.com/ipfs/go-ipns/pb"
)
var log = logging.Logger("ipns")
var _ record.Validator = Validator{}
// RecordKey returns the libp2p record key for a given peer ID.
func RecordKey(pid peer.ID) string {
return "/ipns/" + string(pid)
}
// Validator is an IPNS record validator that satisfies the libp2p record
// validator interface.
type Validator struct {
// KeyBook, if non-nil, will be used to lookup keys for validating IPNS
// records.
KeyBook pstore.KeyBook
}
// Validate validates an IPNS record.
func (v Validator) Validate(key string, value []byte) error {
ns, pidString, err := record.SplitKey(key)
if err != nil || ns != "ipns" {
return ErrInvalidPath
}
// Parse the value into an IpnsEntry
entry := new(pb.IpnsEntry)
err = proto.Unmarshal(value, entry)
if err != nil {
return ErrBadRecord
}
// Get the public key defined by the ipns path
pid, err := peer.IDFromString(pidString)
if err != nil {
log.Debugf("failed to parse ipns record key %s into peer ID", pidString)
return ErrKeyFormat
}
pubk, err := v.getPublicKey(pid, entry)
if err != nil {
return err
}
return Validate(pubk, entry)
}
func (v Validator) getPublicKey(pid peer.ID, entry *pb.IpnsEntry) (ic.PubKey, error) {
pk, err := ExtractPublicKey(pid, entry)
if err != nil {
return nil, err
}
if pk != nil {
return pk, nil
}
if v.KeyBook == nil {
log.Debugf("public key with hash %s not found in IPNS record and no peer store provided", pid)
return nil, ErrPublicKeyNotFound
}
pubk := v.KeyBook.PubKey(pid)
if pubk == nil {
log.Debugf("public key with hash %s not found in peer store", pid)
return nil, ErrPublicKeyNotFound
}
return pubk, nil
}
// Select selects the best record by checking which has the highest sequence
// number and latest EOL.
//
// This function returns an error if any of the records fail to parse. Validate
// your records first!
func (v Validator) Select(k string, vals [][]byte) (int, error) {
var recs []*pb.IpnsEntry
for _, v := range vals {
e := new(pb.IpnsEntry)
if err := proto.Unmarshal(v, e); err != nil {
return -1, err
}
recs = append(recs, e)
}
return selectRecord(recs, vals)
}
func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) {
switch len(recs) {
case 0:
return -1, errors.New("no usable records in given set")
case 1:
return 0, nil
}
var i int
for j := 1; j < len(recs); j++ {
cmp, err := Compare(recs[i], recs[j])
if err != nil {
return -1, err
}
if cmp == 0 {
cmp = bytes.Compare(vals[i], vals[j])
}
if cmp < 0 {
i = j
}
}
return i, nil
}
package ipns
import (
"fmt"
"math/rand"
"testing"
"time"
proto "github.com/gogo/protobuf/proto"
u "github.com/ipfs/go-ipfs-util"
ci "github.com/libp2p/go-libp2p-crypto"
pb "github.com/ipfs/go-ipns/pb"
)
func shuffle(a []*pb.IpnsEntry) {
for n := 0; n < 5; n++ {
for i, _ := range a {
j := rand.Intn(len(a))
a[i], a[j] = a[j], a[i]
}
}
}
func AssertSelected(r *pb.IpnsEntry, from ...*pb.IpnsEntry) error {
shuffle(from)
var vals [][]byte
for _, r := range from {
data, err := proto.Marshal(r)
if err != nil {
return err
}
vals = append(vals, data)
}
i, err := selectRecord(from, vals)
if err != nil {
return err
}
if from[i] != r {
return fmt.Errorf("selected incorrect record %d", i)
}
return nil
}
func TestOrdering(t *testing.T) {
// select timestamp so selection is deterministic
ts := time.Unix(1000000, 0)
// generate a key for signing the records
r := u.NewSeededRand(15) // generate deterministic keypair
priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, r)
if err != nil {
t.Fatal(err)
}
e1, err := Create(priv, []byte("foo"), 1, ts.Add(time.Hour))
if err != nil {
t.Fatal(err)
}
e2, err := Create(priv, []byte("bar"), 2, ts.Add(time.Hour))
if err != nil {
t.Fatal(err)
}
e3, err := Create(priv, []byte("baz"), 3, ts.Add(time.Hour))
if err != nil {
t.Fatal(err)
}
e4, err := Create(priv, []byte("cat"), 3, ts.Add(time.Hour*2))
if err != nil {
t.Fatal(err)
}
e5, err := Create(priv, []byte("dog"), 4, ts.Add(time.Hour*3))
if err != nil {
t.Fatal(err)
}
e6, err := Create(priv, []byte("fish"), 4, ts.Add(time.Hour*3))
if err != nil {
t.Fatal(err)
}
// e1 is the only record, i hope it gets this right
err = AssertSelected(e1, e1)
if err != nil {
t.Fatal(err)
}
// e2 has the highest sequence number
err = AssertSelected(e2, e1, e2)
if err != nil {
t.Fatal(err)
}
// e3 has the highest sequence number
err = AssertSelected(e3, e1, e2, e3)
if err != nil {
t.Fatal(err)
}
// e4 has a higher timeout
err = AssertSelected(e4, e1, e2, e3, e4)
if err != nil {
t.Fatal(err)
}
// e5 has the highest sequence number
err = AssertSelected(e5, e1, e2, e3, e4, e5)
if err != nil {
t.Fatal(err)
}
// e6 should be selected as its signauture will win in the comparison
err = AssertSelected(e6, e1, e2, e3, e4, e5, e6)
if err != nil {
t.Fatal(err)
}
_ = []interface{}{e1, e2, e3, e4, e5, e6}
}
package ipns
import (
"fmt"
"math/rand"
"strings"
"testing"
"time"
pb "github.com/ipfs/go-ipns/pb"
proto "github.com/gogo/protobuf/proto"
u "github.com/ipfs/go-ipfs-util"
ci "github.com/libp2p/go-libp2p-crypto"
peer "github.com/libp2p/go-libp2p-peer"
pstore "github.com/libp2p/go-libp2p-peerstore"
)
func testValidatorCase(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, exp error) {
t.Helper()
match := func(t *testing.T, err error) {
t.Helper()
if err != exp {
params := fmt.Sprintf("key: %s\neol: %s\n", key, eol)
if exp == nil {
t.Fatalf("Unexpected error %s for params %s", err, params)
} else if err == nil {
t.Fatalf("Expected error %s but there was no error for params %s", exp, params)
} else {
t.Fatalf("Expected error %s but got %s for params %s", exp, err, params)
}
}
}
testValidatorCaseMatchFunc(t, priv, kbook, key, val, eol, match)
}
func testValidatorCaseMatchFunc(t *testing.T, priv ci.PrivKey, kbook pstore.KeyBook, key string, val []byte, eol time.Time, matchf func(*testing.T, error)) {
t.Helper()
validator := Validator{kbook}
data := val
if data == nil {
p := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
entry, err := Create(priv, p, 1, eol)
if err != nil {
t.Fatal(err)
}
data, err = proto.Marshal(entry)
if err != nil {
t.Fatal(err)
}
}
matchf(t, validator.Validate(key, data))
}
func TestValidator(t *testing.T) {
ts := time.Now()
priv, id, _ := genKeys(t)
priv2, id2, _ := genKeys(t)
kbook := pstore.NewPeerstore()
kbook.AddPubKey(id, priv.GetPublic())
emptyKbook := pstore.NewPeerstore()
testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), nil)
testValidatorCase(t, priv, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour*-1), ErrExpiredRecord)
testValidatorCase(t, priv, kbook, "/ipns/"+string(id), []byte("bad data"), ts.Add(time.Hour), ErrBadRecord)
testValidatorCase(t, priv, kbook, "/ipns/"+"bad key", nil, ts.Add(time.Hour), ErrKeyFormat)
testValidatorCase(t, priv, emptyKbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrPublicKeyNotFound)
testValidatorCase(t, priv2, kbook, "/ipns/"+string(id2), nil, ts.Add(time.Hour), ErrPublicKeyNotFound)
testValidatorCase(t, priv2, kbook, "/ipns/"+string(id), nil, ts.Add(time.Hour), ErrSignature)
testValidatorCase(t, priv, kbook, "//"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
testValidatorCase(t, priv, kbook, "/wrong/"+string(id), nil, ts.Add(time.Hour), ErrInvalidPath)
}
func mustMarshal(t *testing.T, entry *pb.IpnsEntry) []byte {
t.Helper()
data, err := proto.Marshal(entry)
if err != nil {
t.Fatal(err)
}
return data
}
func TestEmbeddedPubKeyValidate(t *testing.T) {
goodeol := time.Now().Add(time.Hour)
kbook := pstore.NewPeerstore()
pth := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
priv, _, ipnsk := genKeys(t)
entry, err := Create(priv, pth, 1, goodeol)
if err != nil {
t.Fatal(err)
}
testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyNotFound)
pubkb, err := priv.GetPublic().Bytes()
if err != nil {
t.Fatal(err)
}
entry.PubKey = pubkb
testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, nil)
entry.PubKey = []byte("probably not a public key")
testValidatorCaseMatchFunc(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, func(t *testing.T, err error) {
if !strings.Contains(err.Error(), "unmarshaling pubkey in record:") {
t.Fatal("expected pubkey unmarshaling to fail")
}
})
opriv, _, _ := genKeys(t)
wrongkeydata, err := opriv.GetPublic().Bytes()
if err != nil {
t.Fatal(err)
}
entry.PubKey = wrongkeydata
testValidatorCase(t, priv, kbook, ipnsk, mustMarshal(t, entry), goodeol, ErrPublicKeyMismatch)
}
func TestPeerIDPubKeyValidate(t *testing.T) {
goodeol := time.Now().Add(time.Hour)
kbook := pstore.NewPeerstore()
pth := []byte("/ipfs/QmfM2r8seH2GiRaC4esTjeraXEachRt8ZsSeGaWTPLyMoG")
sk, pk, err := ci.GenerateEd25519Key(rand.New(rand.NewSource(42)))
if err != nil {
t.Fatal(err)
}
pid, err := peer.IDFromPublicKey(pk)
if err != nil {
t.Fatal(err)
}
ipnsk := "/ipns/" + string(pid)
entry, err := Create(sk, pth, 1, goodeol)
if err != nil {
t.Fatal(err)
}
dataNoKey, err := proto.Marshal(entry)
if err != nil {
t.Fatal(err)
}
testValidatorCase(t, sk, kbook, ipnsk, dataNoKey, goodeol, nil)
}
func genKeys(t *testing.T) (ci.PrivKey, peer.ID, string) {
sr := u.NewTimeSeededRand()
priv, _, err := ci.GenerateKeyPairWithReader(ci.RSA, 1024, sr)
if err != nil {
t.Fatal(err)
}
// Create entry with expiry in one hour
pid, err := peer.IDFromPrivateKey(priv)
if err != nil {
t.Fatal(err)
}
ipnsKey := RecordKey(pid)
return priv, pid, ipnsKey
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment