2015-03-25 07:16:52 +00:00
|
|
|
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2015-11-06 16:36:59 +00:00
|
|
|
"bytes"
|
2015-09-13 09:44:33 +00:00
|
|
|
"crypto/tls"
|
2015-03-25 07:16:52 +00:00
|
|
|
"database/sql"
|
2015-09-13 09:44:33 +00:00
|
|
|
"encoding/json"
|
2015-11-06 16:36:59 +00:00
|
|
|
"encoding/pem"
|
2015-03-25 07:16:52 +00:00
|
|
|
"log"
|
|
|
|
"net"
|
2015-09-13 09:44:33 +00:00
|
|
|
"net/http"
|
2015-07-21 22:56:27 +00:00
|
|
|
"net/url"
|
2015-11-13 08:13:53 +00:00
|
|
|
"sync"
|
2015-03-25 07:16:52 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/golang/groupcache/lru"
|
|
|
|
"github.com/juju/ratelimit"
|
2015-09-22 17:44:40 +00:00
|
|
|
"github.com/syncthing/syncthing/lib/protocol"
|
2015-03-25 07:16:52 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type querysrv struct {
|
2015-09-13 09:44:33 +00:00
|
|
|
addr string
|
|
|
|
db *sql.DB
|
|
|
|
prep map[string]*sql.Stmt
|
2015-11-13 08:13:53 +00:00
|
|
|
limiter *safeCache
|
2015-09-13 09:44:33 +00:00
|
|
|
cert tls.Certificate
|
|
|
|
listener net.Listener
|
|
|
|
}
|
|
|
|
|
|
|
|
type announcement struct {
|
|
|
|
Direct []string `json:"direct"`
|
|
|
|
Relays []annRelay `json:"relays"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type annRelay struct {
|
|
|
|
URL string `json:"url"`
|
|
|
|
Latency int `json:"latency"`
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-11-13 08:13:53 +00:00
|
|
|
type safeCache struct {
|
|
|
|
*lru.Cache
|
|
|
|
mut sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *safeCache) Get(key string) (val interface{}, ok bool) {
|
|
|
|
s.mut.Lock()
|
|
|
|
val, ok = s.Cache.Get(key)
|
|
|
|
s.mut.Unlock()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *safeCache) Add(key string, val interface{}) {
|
|
|
|
s.mut.Lock()
|
|
|
|
s.Cache.Add(key, val)
|
|
|
|
s.mut.Unlock()
|
|
|
|
}
|
|
|
|
|
2015-03-25 07:16:52 +00:00
|
|
|
func (s *querysrv) Serve() {
|
2015-11-13 08:13:53 +00:00
|
|
|
s.limiter = &safeCache{
|
|
|
|
Cache: lru.New(lruSize),
|
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-11-06 16:36:59 +00:00
|
|
|
if useHttp {
|
|
|
|
listener, err := net.Listen("tcp", s.addr)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Listen:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
s.listener = listener
|
|
|
|
} else {
|
|
|
|
tlsCfg := &tls.Config{
|
|
|
|
Certificates: []tls.Certificate{s.cert},
|
|
|
|
ClientAuth: tls.RequestClientCert,
|
|
|
|
SessionTicketsDisabled: true,
|
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
CipherSuites: []uint16{
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Listen:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
s.listener = tlsListener
|
2015-09-13 09:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
http.HandleFunc("/", s.handler)
|
2015-09-21 10:49:17 +00:00
|
|
|
http.HandleFunc("/ping", handlePing)
|
2015-09-13 09:44:33 +00:00
|
|
|
|
|
|
|
srv := &http.Server{
|
2015-11-06 10:20:28 +00:00
|
|
|
ReadTimeout: 5 * time.Second,
|
|
|
|
WriteTimeout: 5 * time.Second,
|
|
|
|
MaxHeaderBytes: 1 << 10,
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
|
2015-11-06 16:36:59 +00:00
|
|
|
if err := srv.Serve(s.listener); err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
log.Println("Serve:", err)
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
|
|
|
|
if debug {
|
|
|
|
log.Println(req.Method, req.URL)
|
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-11-06 16:36:59 +00:00
|
|
|
var remoteIP net.IP
|
|
|
|
if useHttp {
|
|
|
|
remoteIP = net.ParseIP(req.Header.Get("X-Forwarded-For"))
|
|
|
|
} else {
|
|
|
|
addr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("remoteAddr:", err)
|
|
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
remoteIP = addr.IP
|
2015-09-13 09:44:33 +00:00
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-11-06 16:36:59 +00:00
|
|
|
if s.limit(remoteIP) {
|
2015-09-13 09:44:33 +00:00
|
|
|
if debug {
|
2015-11-06 16:36:59 +00:00
|
|
|
log.Println(remoteIP, "is limited")
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
w.Header().Set("Retry-After", "60")
|
|
|
|
http.Error(w, "Too Many Requests", 429)
|
|
|
|
return
|
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
switch req.Method {
|
|
|
|
case "GET":
|
|
|
|
s.handleGET(w, req)
|
|
|
|
case "POST":
|
2015-11-06 16:36:59 +00:00
|
|
|
s.handlePOST(remoteIP, w, req)
|
2015-09-13 09:44:33 +00:00
|
|
|
default:
|
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
func (s *querysrv) handleGET(w http.ResponseWriter, req *http.Request) {
|
|
|
|
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
|
|
|
|
if err != nil {
|
|
|
|
if debug {
|
|
|
|
log.Println(req.Method, req.URL, "bad device param")
|
|
|
|
}
|
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
var ann announcement
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
ann.Direct, err = s.getAddresses(deviceID)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("getAddresses:", err)
|
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
ann.Relays, err = s.getRelays(deviceID)
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
log.Println("getRelays:", err)
|
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-11-06 10:21:28 +00:00
|
|
|
globalStats.Query()
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
if len(ann.Direct)+len(ann.Relays) == 0 {
|
|
|
|
http.Error(w, "Not Found", http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
2015-07-21 22:56:27 +00:00
|
|
|
|
2015-11-06 10:21:28 +00:00
|
|
|
globalStats.Answer()
|
2015-09-13 09:44:33 +00:00
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
json.NewEncoder(w).Encode(ann)
|
|
|
|
}
|
|
|
|
|
2015-11-06 16:36:59 +00:00
|
|
|
func (s *querysrv) handlePOST(remoteIP net.IP, w http.ResponseWriter, req *http.Request) {
|
|
|
|
rawCert := certificateBytes(req)
|
|
|
|
if rawCert == nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
if debug {
|
|
|
|
log.Println(req.Method, req.URL, "no certificates")
|
2015-07-21 22:56:27 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
2015-07-21 22:56:27 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
var ann announcement
|
|
|
|
if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
|
|
|
|
if debug {
|
|
|
|
log.Println(req.Method, req.URL, err)
|
2015-07-21 22:56:27 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
|
|
|
return
|
2015-07-21 22:56:27 +00:00
|
|
|
}
|
|
|
|
|
2015-11-06 16:36:59 +00:00
|
|
|
deviceID := protocol.NewDeviceID(rawCert)
|
2015-09-13 09:44:33 +00:00
|
|
|
|
|
|
|
// handleAnnounce returns *two* errors. The first indicates a problem with
|
|
|
|
// something the client posted to us. We should return a 400 Bad Request
|
|
|
|
// and not worry about it. The second indicates that the request was fine,
|
|
|
|
// but something internal fucked up. We should log it and respond with a
|
|
|
|
// more apologetic 500 Internal Server Error.
|
2015-11-06 16:36:59 +00:00
|
|
|
userErr, internalErr := s.handleAnnounce(remoteIP, deviceID, ann.Direct, ann.Relays)
|
2015-09-13 09:44:33 +00:00
|
|
|
if userErr != nil {
|
|
|
|
if debug {
|
|
|
|
log.Println(req.Method, req.URL, userErr)
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
if internalErr != nil {
|
|
|
|
log.Println("handleAnnounce:", internalErr)
|
|
|
|
globalStats.Error()
|
|
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
globalStats.Announce()
|
|
|
|
|
|
|
|
// TODO: Slowly increase this for stable clients
|
|
|
|
w.Header().Set("Reannounce-After", "1800")
|
|
|
|
|
|
|
|
// We could return the lookup result here, but it's kind of unnecessarily
|
|
|
|
// expensive to go query the database again so we let the client decide to
|
|
|
|
// do a lookup if they really care.
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *querysrv) Stop() {
|
|
|
|
s.listener.Close()
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
func (s *querysrv) handleAnnounce(remote net.IP, deviceID protocol.DeviceID, direct []string, relays []annRelay) (userErr, internalErr error) {
|
|
|
|
tx, err := s.db.Begin()
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
internalErr = err
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
defer func() {
|
|
|
|
// Since we return from a bunch of different places, we handle
|
|
|
|
// rollback in the defer.
|
|
|
|
if internalErr != nil || userErr != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
}
|
|
|
|
}()
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
for _, annAddr := range direct {
|
|
|
|
uri, err := url.Parse(annAddr)
|
|
|
|
if err != nil {
|
|
|
|
userErr = err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
host, port, err := net.SplitHostPort(uri.Host)
|
|
|
|
if err != nil {
|
|
|
|
userErr = err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ip := net.ParseIP(host)
|
|
|
|
if len(ip) == 0 || ip.IsUnspecified() {
|
|
|
|
uri.Host = net.JoinHostPort(remote.String(), port)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.updateAddress(tx, deviceID, uri.String()); err != nil {
|
|
|
|
internalErr = err
|
|
|
|
return
|
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
_, err = tx.Stmt(s.prep["deleteRelay"]).Exec(deviceID.String())
|
2015-07-21 22:56:27 +00:00
|
|
|
if err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
internalErr = err
|
|
|
|
return
|
2015-07-21 22:56:27 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
for _, relay := range relays {
|
|
|
|
uri, err := url.Parse(relay.URL)
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
userErr = err
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
|
|
|
|
_, err = tx.Stmt(s.prep["insertRelay"]).Exec(deviceID.String(), uri.String(), relay.Latency)
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
internalErr = err
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
}
|
2015-03-25 07:16:52 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
if err := s.updateDevice(tx, deviceID); err != nil {
|
|
|
|
internalErr = err
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
internalErr = tx.Commit()
|
|
|
|
return
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
func (s *querysrv) limit(remote net.IP) bool {
|
|
|
|
key := remote.String()
|
2015-03-25 07:16:52 +00:00
|
|
|
|
|
|
|
bkt, ok := s.limiter.Get(key)
|
|
|
|
if ok {
|
|
|
|
bkt := bkt.(*ratelimit.Bucket)
|
|
|
|
if bkt.TakeAvailable(1) != 1 {
|
|
|
|
// Rate limit exceeded; ignore packet
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// One packet per ten seconds average rate, burst ten packets
|
|
|
|
s.limiter.Add(key, ratelimit.NewBucket(10*time.Second/time.Duration(limitAvg), int64(limitBurst)))
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *querysrv) updateDevice(tx *sql.Tx, device protocol.DeviceID) error {
|
|
|
|
res, err := tx.Stmt(s.prep["updateDevice"]).Exec(device.String())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if rows, _ := res.RowsAffected(); rows == 0 {
|
|
|
|
_, err := tx.Stmt(s.prep["insertDevice"]).Exec(device.String())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-07-21 22:56:27 +00:00
|
|
|
func (s *querysrv) updateAddress(tx *sql.Tx, device protocol.DeviceID, uri string) error {
|
|
|
|
res, err := tx.Stmt(s.prep["updateAddress"]).Exec(device.String(), uri)
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if rows, _ := res.RowsAffected(); rows == 0 {
|
2015-07-21 22:56:27 +00:00
|
|
|
_, err := tx.Stmt(s.prep["insertAddress"]).Exec(device.String(), uri)
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-07-21 22:56:27 +00:00
|
|
|
func (s *querysrv) getAddresses(device protocol.DeviceID) ([]string, error) {
|
2015-03-25 07:16:52 +00:00
|
|
|
rows, err := s.prep["selectAddress"].Query(device.String())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-07-21 22:56:27 +00:00
|
|
|
var res []string
|
2015-03-25 07:16:52 +00:00
|
|
|
for rows.Next() {
|
|
|
|
var addr string
|
2015-07-21 22:56:27 +00:00
|
|
|
|
|
|
|
err := rows.Scan(&addr)
|
2015-03-25 07:16:52 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println("Scan:", err)
|
|
|
|
continue
|
|
|
|
}
|
2015-07-21 22:56:27 +00:00
|
|
|
res = append(res, addr)
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
func (s *querysrv) getRelays(device protocol.DeviceID) ([]annRelay, error) {
|
2015-07-21 22:56:27 +00:00
|
|
|
rows, err := s.prep["selectRelay"].Query(device.String())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
var res []annRelay
|
2015-07-21 22:56:27 +00:00
|
|
|
for rows.Next() {
|
2015-09-13 09:44:33 +00:00
|
|
|
var rel annRelay
|
2015-07-21 22:56:27 +00:00
|
|
|
|
2015-09-13 09:44:33 +00:00
|
|
|
err := rows.Scan(&rel.URL, &rel.Latency)
|
2015-07-21 22:56:27 +00:00
|
|
|
if err != nil {
|
2015-09-13 09:44:33 +00:00
|
|
|
return nil, err
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
2015-09-13 09:44:33 +00:00
|
|
|
res = append(res, rel)
|
2015-03-25 07:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return res, nil
|
|
|
|
}
|
2015-09-21 10:49:17 +00:00
|
|
|
|
|
|
|
func handlePing(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(204)
|
|
|
|
}
|
2015-11-06 16:36:59 +00:00
|
|
|
|
|
|
|
func certificateBytes(req *http.Request) []byte {
|
|
|
|
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
|
|
|
|
return req.TLS.PeerCertificates[0].Raw
|
|
|
|
}
|
|
|
|
|
|
|
|
if hdr := req.Header.Get("X-SSL-Cert"); hdr != "" {
|
|
|
|
bs := []byte(hdr)
|
|
|
|
// The certificate is in PEM format but with spaces for newlines. We
|
|
|
|
// need to reinstate the newlines for the PEM decoder. But we need to
|
|
|
|
// leave the spaces in the BEGIN and END lines - the first and last
|
|
|
|
// space - alone.
|
|
|
|
firstSpace := bytes.Index(bs, []byte(" "))
|
|
|
|
lastSpace := bytes.LastIndex(bs, []byte(" "))
|
|
|
|
for i := firstSpace + 1; i < lastSpace; i++ {
|
|
|
|
if bs[i] == ' ' {
|
|
|
|
bs[i] = '\n'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
block, _ := pem.Decode(bs)
|
|
|
|
if block == nil {
|
|
|
|
// Decoding failed
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return block.Bytes
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|