2010-06-12 21:45:58 +00:00
|
|
|
// Copyright 2010 Brad Fitzpatrick <brad@danga.com>
|
|
|
|
//
|
|
|
|
// See LICENSE.
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import "crypto/sha1"
|
2010-06-14 04:51:18 +00:00
|
|
|
import "encoding/base64"
|
2010-06-12 21:45:58 +00:00
|
|
|
import "flag"
|
|
|
|
import "fmt"
|
|
|
|
import "hash"
|
|
|
|
import "http"
|
|
|
|
import "io"
|
|
|
|
import "io/ioutil"
|
|
|
|
import "os"
|
|
|
|
import "regexp"
|
2010-07-07 04:57:53 +00:00
|
|
|
|
2010-07-09 04:19:54 +00:00
|
|
|
import "./util/_obj/util"
|
|
|
|
|
2010-07-07 04:57:53 +00:00
|
|
|
// import "mime/multipart"
|
2010-07-02 23:50:07 +00:00
|
|
|
// import multipart "github.com/bradfitz/golang-mime-multipart"
|
2010-06-12 21:45:58 +00:00
|
|
|
|
|
|
|
var listen *string = flag.String("listen", "0.0.0.0:3179", "host:port to listen on")
|
|
|
|
var storageRoot *string = flag.String("root", "/tmp/camliroot", "Root directory to store files")
|
|
|
|
|
2010-06-21 01:26:54 +00:00
|
|
|
var putPassword string
|
2010-06-12 21:45:58 +00:00
|
|
|
|
2010-06-13 00:15:49 +00:00
|
|
|
var kGetPutPattern *regexp.Regexp = regexp.MustCompile(`^/camli/(sha1)-([a-f0-9]+)$`)
|
2010-06-14 04:51:18 +00:00
|
|
|
var kBasicAuthPattern *regexp.Regexp = regexp.MustCompile(`^Basic ([a-zA-Z0-9\+/=]+)`)
|
2010-07-07 04:57:53 +00:00
|
|
|
|
|
|
|
var kBlobRefPattern *regexp.Regexp = regexp.MustCompile(`^([a-z0-9]+)-([a-f0-9]+)$`)
|
2010-06-21 06:05:50 +00:00
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
type BlobRef struct {
|
|
|
|
HashName string
|
|
|
|
Digest string
|
|
|
|
}
|
2010-06-13 00:15:49 +00:00
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
func ParsePath(path string) *BlobRef {
|
2010-06-13 00:15:49 +00:00
|
|
|
groups := kGetPutPattern.MatchStrings(path)
|
2010-06-21 06:05:50 +00:00
|
|
|
if len(groups) != 3 {
|
2010-06-13 00:15:49 +00:00
|
|
|
return nil
|
|
|
|
}
|
2010-06-21 00:49:23 +00:00
|
|
|
obj := &BlobRef{groups[1], groups[2]}
|
|
|
|
if obj.HashName == "sha1" && len(obj.Digest) != 40 {
|
2010-06-13 00:15:49 +00:00
|
|
|
return nil
|
|
|
|
}
|
2010-07-07 04:57:53 +00:00
|
|
|
return obj
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
func (o *BlobRef) IsSupported() bool {
|
|
|
|
if o.HashName == "sha1" {
|
2010-06-13 00:15:49 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
func (o *BlobRef) Hash() hash.Hash {
|
|
|
|
if o.HashName == "sha1" {
|
2010-06-13 00:15:49 +00:00
|
|
|
return sha1.New()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
func (o *BlobRef) FileBaseName() string {
|
|
|
|
return fmt.Sprintf("%s-%s.dat", o.HashName, o.Digest)
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
func (o *BlobRef) DirectoryName() string {
|
|
|
|
return fmt.Sprintf("%s/%s/%s", *storageRoot, o.Digest[0:3], o.Digest[3:6])
|
2010-06-13 00:15:49 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2010-06-21 00:49:23 +00:00
|
|
|
func (o *BlobRef) FileName() string {
|
|
|
|
return fmt.Sprintf("%s/%s-%s.dat", o.DirectoryName(), o.HashName, o.Digest)
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
2010-06-12 21:45:58 +00:00
|
|
|
|
|
|
|
func badRequestError(conn *http.Conn, errorMessage string) {
|
|
|
|
conn.WriteHeader(http.StatusBadRequest)
|
2010-07-07 04:57:53 +00:00
|
|
|
fmt.Fprintf(conn, "%s\n", errorMessage)
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func serverError(conn *http.Conn, err os.Error) {
|
|
|
|
conn.WriteHeader(http.StatusInternalServerError)
|
|
|
|
fmt.Fprintf(conn, "Server error: %s\n", err)
|
|
|
|
}
|
|
|
|
|
2010-06-14 04:51:18 +00:00
|
|
|
func putAllowed(req *http.Request) bool {
|
|
|
|
auth, present := req.Header["Authorization"]
|
|
|
|
if !present {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
matches := kBasicAuthPattern.MatchStrings(auth)
|
|
|
|
if len(matches) != 2 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
var outBuf []byte = make([]byte, base64.StdEncoding.DecodedLen(len(matches[1])))
|
|
|
|
bytes, err := base64.StdEncoding.Decode(outBuf, []uint8(matches[1]))
|
2010-06-21 01:26:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
password := string(outBuf)
|
2010-06-14 04:51:18 +00:00
|
|
|
fmt.Println("Decoded bytes:", bytes, " error: ", err)
|
2010-06-21 01:26:54 +00:00
|
|
|
fmt.Println("Got userPass:", password)
|
2010-07-07 04:57:53 +00:00
|
|
|
return password != "" && password == putPassword
|
2010-06-21 01:26:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func getAllowed(req *http.Request) bool {
|
|
|
|
// For now...
|
|
|
|
return putAllowed(req)
|
2010-06-14 04:51:18 +00:00
|
|
|
}
|
|
|
|
|
2010-07-02 23:50:07 +00:00
|
|
|
func handleCamliForm(conn *http.Conn, req *http.Request) {
|
|
|
|
fmt.Fprintf(conn, `
|
|
|
|
<html>
|
|
|
|
<body>
|
2010-07-07 04:57:53 +00:00
|
|
|
<form method='POST' enctype="multipart/form-data" action="/camli/testform">
|
2010-07-02 23:50:07 +00:00
|
|
|
<input type="hidden" name="имя" value="брэд" />
|
|
|
|
Text unix: <input type="file" name="file-unix"><br>
|
|
|
|
Text win: <input type="file" name="file-win"><br>
|
|
|
|
Text mac: <input type="file" name="file-mac"><br>
|
|
|
|
Image png: <input type="file" name="image-png"><br>
|
|
|
|
<input type=submit>
|
|
|
|
</form>
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`)
|
|
|
|
}
|
|
|
|
|
2010-06-12 21:45:58 +00:00
|
|
|
func handleCamli(conn *http.Conn, req *http.Request) {
|
2010-06-21 06:05:50 +00:00
|
|
|
if req.Method == "POST" && req.URL.Path == "/camli/upload" {
|
2010-07-07 04:57:53 +00:00
|
|
|
handleMultiPartUpload(conn, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.Method == "POST" && req.URL.Path == "/camli/testform" {
|
|
|
|
handleTestForm(conn, req)
|
2010-06-21 06:05:50 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-07-02 23:50:07 +00:00
|
|
|
if req.Method == "GET" && req.URL.Path == "/camli/form" {
|
2010-07-07 04:57:53 +00:00
|
|
|
handleCamliForm(conn, req)
|
2010-07-02 23:50:07 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-06-21 06:05:50 +00:00
|
|
|
if req.Method == "PUT" {
|
2010-06-12 21:45:58 +00:00
|
|
|
handlePut(conn, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-06-21 06:05:50 +00:00
|
|
|
if req.Method == "GET" {
|
2010-06-13 00:15:49 +00:00
|
|
|
handleGet(conn, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-06-12 21:45:58 +00:00
|
|
|
badRequestError(conn, "Unsupported method.")
|
|
|
|
}
|
|
|
|
|
2010-06-13 00:15:49 +00:00
|
|
|
func handleGet(conn *http.Conn, req *http.Request) {
|
2010-06-21 01:26:54 +00:00
|
|
|
if !getAllowed(req) {
|
|
|
|
conn.SetHeader("WWW-Authenticate", "Basic realm=\"camlistored\"")
|
|
|
|
conn.WriteHeader(http.StatusUnauthorized)
|
|
|
|
fmt.Fprintf(conn, "Authentication required.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-07-07 04:57:53 +00:00
|
|
|
blobRef := ParsePath(req.URL.Path)
|
|
|
|
if blobRef == nil {
|
2010-06-13 00:15:49 +00:00
|
|
|
badRequestError(conn, "Malformed GET URL.")
|
2010-07-07 04:57:53 +00:00
|
|
|
return
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
2010-07-07 04:57:53 +00:00
|
|
|
fileName := blobRef.FileName()
|
2010-06-13 00:15:49 +00:00
|
|
|
stat, err := os.Stat(fileName)
|
|
|
|
if err == os.ENOENT {
|
|
|
|
conn.WriteHeader(http.StatusNotFound)
|
|
|
|
fmt.Fprintf(conn, "Object not found.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
serverError(conn, err)
|
|
|
|
return
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
|
|
|
file, err := os.Open(fileName, os.O_RDONLY, 0)
|
|
|
|
if err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
serverError(conn, err)
|
|
|
|
return
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
|
|
|
conn.SetHeader("Content-Type", "application/octet-stream")
|
|
|
|
bytesCopied, err := io.Copy(conn, file)
|
|
|
|
|
|
|
|
// If there's an error at this point, it's too late to tell the client,
|
|
|
|
// as they've already been receiving bytes. But they should be smart enough
|
|
|
|
// to verify the digest doesn't match. But we close the (chunked) response anyway,
|
|
|
|
// to further signal errors.
|
2010-07-07 04:57:53 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "Error sending file: %v, err=%v\n", blobRef, err)
|
2010-06-13 00:15:49 +00:00
|
|
|
closer, _, err := conn.Hijack()
|
2010-07-07 04:57:53 +00:00
|
|
|
if err != nil {
|
|
|
|
closer.Close()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2010-06-13 00:15:49 +00:00
|
|
|
if bytesCopied != stat.Size {
|
2010-07-07 04:57:53 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "Error sending file: %v, copied= %d, not %d%v\n", blobRef,
|
2010-06-13 00:15:49 +00:00
|
|
|
bytesCopied, stat.Size)
|
|
|
|
closer, _, err := conn.Hijack()
|
2010-07-07 04:57:53 +00:00
|
|
|
if err != nil {
|
|
|
|
closer.Close()
|
|
|
|
}
|
|
|
|
return
|
2010-06-13 00:15:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-07-07 04:57:53 +00:00
|
|
|
func handleTestForm(conn *http.Conn, req *http.Request) {
|
|
|
|
if !(req.Method == "POST" && req.URL.Path == "/camli/testform") {
|
2010-06-21 06:05:50 +00:00
|
|
|
badRequestError(conn, "Inconfigured handler.")
|
|
|
|
return
|
|
|
|
}
|
2010-07-07 04:57:53 +00:00
|
|
|
|
|
|
|
multipart, err := req.MultipartReader()
|
|
|
|
if multipart == nil {
|
|
|
|
badRequestError(conn, fmt.Sprintf("Expected multipart/form-data POST request; %v", err))
|
|
|
|
return
|
2010-06-21 06:05:50 +00:00
|
|
|
}
|
|
|
|
|
2010-06-29 17:37:05 +00:00
|
|
|
for {
|
2010-07-07 04:57:53 +00:00
|
|
|
part, err := multipart.NextPart()
|
2010-06-29 17:37:05 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Error reading:", err)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if part == nil {
|
|
|
|
break
|
|
|
|
}
|
2010-07-07 04:57:53 +00:00
|
|
|
formName := part.FormName()
|
|
|
|
fmt.Printf("New value [%s], part=%v\n", formName, part)
|
2010-07-02 23:50:07 +00:00
|
|
|
|
|
|
|
sha1 := sha1.New()
|
|
|
|
io.Copy(sha1, part)
|
|
|
|
fmt.Printf("Got part digest: %x\n", sha1.Sum())
|
|
|
|
|
2010-06-29 17:37:05 +00:00
|
|
|
}
|
|
|
|
fmt.Println("Done reading multipart body.")
|
2010-07-07 04:57:53 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleMultiPartUpload(conn *http.Conn, req *http.Request) {
|
|
|
|
if !(req.Method == "POST" && req.URL.Path == "/camli/upload") {
|
|
|
|
badRequestError(conn, "Inconfigured handler.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
multipart, err := req.MultipartReader()
|
|
|
|
if multipart == nil {
|
|
|
|
badRequestError(conn, fmt.Sprintf(
|
|
|
|
"Expected multipart/form-data POST request; %v", err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
part, err := multipart.NextPart()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Error reading multipart section:", err)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if part == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
formName := part.FormName()
|
|
|
|
fmt.Printf("New value [%s], part=%v\n", formName, part)
|
|
|
|
|
|
|
|
matches := kBlobRefPattern.MatchStrings(formName)
|
|
|
|
if len(matches) != 3 {
|
|
|
|
fmt.Printf("Ignoring form key [%s]\n", formName)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ref := &BlobRef{matches[1], matches[2]}
|
|
|
|
|
|
|
|
ok, err := receiveBlob(ref, part)
|
|
|
|
if !ok {
|
|
|
|
fmt.Printf("Error receiving blob %v: %v\n", ref, err)
|
|
|
|
} else {
|
|
|
|
fmt.Printf("Received blob %v\n", ref)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fmt.Println("Done reading multipart body.")
|
2010-06-21 06:05:50 +00:00
|
|
|
}
|
|
|
|
|
2010-07-09 04:19:54 +00:00
|
|
|
func receiveBlob(blobRef *BlobRef, source io.Reader) (ok bool, err os.Error) {
|
2010-07-07 04:57:53 +00:00
|
|
|
hashedDirectory := blobRef.DirectoryName()
|
2010-07-09 04:19:54 +00:00
|
|
|
err = os.MkdirAll(hashedDirectory, 0700)
|
2010-06-12 21:45:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-07-09 04:19:54 +00:00
|
|
|
var tempFile *os.File
|
|
|
|
tempFile, err = ioutil.TempFile(hashedDirectory, blobRef.FileBaseName()+".tmp")
|
2010-06-12 21:45:58 +00:00
|
|
|
if err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
return
|
|
|
|
}
|
2010-06-12 21:45:58 +00:00
|
|
|
|
2010-07-07 04:57:53 +00:00
|
|
|
success := false // set true later
|
2010-06-12 21:45:58 +00:00
|
|
|
defer func() {
|
|
|
|
if !success {
|
|
|
|
fmt.Println("Removing temp file: ", tempFile.Name())
|
|
|
|
os.Remove(tempFile.Name())
|
|
|
|
}
|
2010-07-07 04:57:53 +00:00
|
|
|
}()
|
2010-06-12 21:45:58 +00:00
|
|
|
|
2010-07-09 04:19:54 +00:00
|
|
|
sha1 := sha1.New()
|
|
|
|
var written int64
|
|
|
|
written, err = io.Copy(util.NewTee(sha1, tempFile), source)
|
2010-06-12 21:45:58 +00:00
|
|
|
if err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
return
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
if err = tempFile.Close(); err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
return
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
|
2010-07-07 04:57:53 +00:00
|
|
|
fileName := blobRef.FileName()
|
2010-06-12 21:45:58 +00:00
|
|
|
if err = os.Rename(tempFile.Name(), fileName); err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
return
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
stat, err := os.Lstat(fileName)
|
|
|
|
if err != nil {
|
2010-07-07 04:57:53 +00:00
|
|
|
return
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
if !stat.IsRegular() || stat.Size != written {
|
2010-07-09 04:19:54 +00:00
|
|
|
return false, os.NewError("Written size didn't match.")
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
success = true
|
2010-07-09 04:19:54 +00:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func handlePut(conn *http.Conn, req *http.Request) {
|
|
|
|
blobRef := ParsePath(req.URL.Path)
|
|
|
|
if blobRef == nil {
|
|
|
|
badRequestError(conn, "Malformed PUT URL.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !blobRef.IsSupported() {
|
|
|
|
badRequestError(conn, "unsupported object hash function")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !putAllowed(req) {
|
|
|
|
conn.SetHeader("WWW-Authenticate", "Basic realm=\"camlistored\"")
|
|
|
|
conn.WriteHeader(http.StatusUnauthorized)
|
|
|
|
fmt.Fprintf(conn, "Authentication required.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(bradfitz): authn/authz checks here.
|
|
|
|
|
|
|
|
_, err := receiveBlob(blobRef, req.Body)
|
|
|
|
if err != nil {
|
|
|
|
serverError(conn, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2010-06-12 21:45:58 +00:00
|
|
|
fmt.Fprint(conn, "OK")
|
|
|
|
}
|
|
|
|
|
|
|
|
func HandleRoot(conn *http.Conn, req *http.Request) {
|
|
|
|
fmt.Fprintf(conn, `
|
|
|
|
This is camlistored, a Camlistore storage daemon.
|
2010-07-07 04:57:53 +00:00
|
|
|
`)
|
2010-06-12 21:45:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
flag.Parse()
|
|
|
|
|
2010-06-21 01:26:54 +00:00
|
|
|
putPassword = os.Getenv("CAMLI_PASSWORD")
|
|
|
|
if len(putPassword) == 0 {
|
2010-06-12 21:45:58 +00:00
|
|
|
fmt.Fprintf(os.Stderr,
|
|
|
|
"No CAMLI_PASSWORD environment variable set.\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
fi, err := os.Stat(*storageRoot)
|
|
|
|
if err != nil || !fi.IsDirectory() {
|
|
|
|
fmt.Fprintf(os.Stderr,
|
|
|
|
"Storage root '%s' doesn't exist or is not a directory.\n",
|
|
|
|
*storageRoot)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("/", HandleRoot)
|
|
|
|
mux.HandleFunc("/camli/", handleCamli)
|
|
|
|
|
|
|
|
fmt.Printf("Starting to listen on http://%v/\n", *listen)
|
|
|
|
err := http.ListenAndServe(*listen, mux)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr,
|
|
|
|
"Error in http server: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|