// MIT License // Copyright (c) 2016 @zet4 / @Zeta#2229 / <my-name-is-zeta@and.my.foxgirlsare.sexy> // Copyright © 2018 @kura // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package owogo import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "mime" "mime/multipart" "net/http" "net/textproto" "net/url" "path" "strings" ) var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } // Client stores http client and key type Client struct { Key string APIRoot string APIFileUploadEndpoint string APIShortenEndpoint string client *http.Client } // NewOwOClient returns a fully configured client with official owo endpoints func NewOwOClient(key string, client *http.Client) *Client { return NewClient(key, "", "", "", client) } // NewClient returns a fully configured client func NewClient(key, root, upload, shorten string, client *http.Client) *Client { if client == nil { client = http.DefaultClient } if root == "" { root = OfficialAPIRoot } if upload == "" { upload = APIFileUploadEndpoint } if shorten == "" { shorten = APIShortenEndpoint } return &Client{client: client, Key: key, APIRoot: root, APIFileUploadEndpoint: upload, APIShortenEndpoint: shorten} } // UploadFilePaths uploads files by path func (o *Client) UploadFilePaths(ctx context.Context, files ...string) (*Response, error) { var readers []*NamedReader for _, file := range files { reader, err := NewNamedReaderFromFilesystem(file) if err != nil { return nil, err } readers = append(readers, reader) } return o.UploadFiles(ctx, readers...) } // UploadFiles uploads files func (o *Client) UploadFiles(ctx context.Context, rs ...*NamedReader) (response *Response, err error) { if len(rs) > FileCountLimit { return nil, ErrToManyFiles } body := &bytes.Buffer{} writer := multipart.NewWriter(body) for _, r := range rs { h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="files[]"; filename="%s"`, escapeQuotes(r.Filename))) var buf bytes.Buffer var b []byte tee := io.TeeReader(r.Reader, &buf) b, err = ioutil.ReadAll(tee) if err != nil { return } contenttype := http.DetectContentType(b) if contenttype == "application/octet-stream" { contenttype = mime.TypeByExtension(path.Ext(r.Filename)) if contenttype == "" { contenttype = "application/octet-stream" } } h.Set("Content-Type", contenttype) // no error checking necessary // - `writer` uses `body` (writes to bytes.Buffer). It only throws if it runs out of memory. part, _ := writer.CreatePart(h) // no error checking necessary // - `part` is a valid destination (indirect write to bytes.Buffer). It only throws if it runs out of memory. // - `buf` is always a valid source (bytes.Buffer). It always returns data or EOF (which is not an error for Copy). io.Copy(part, &buf) } // no error checking necessary // - `writer` uses `body` (writes to bytes.Buffer). It only throws if it runs out of memory. writer.Close() req, err := http.NewRequest("POST", o.APIRoot+o.APIFileUploadEndpoint, body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", o.Key) resp, err := o.client.Do(req) if err != nil { return } err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return } if !response.Success { return nil, ErrRequestFailed } return } // ShortenURLs shortens urls func (o *Client) ShortenURLs(ctx context.Context, urls ...string) (shortened []string, err error) { for _, u := range urls { v := url.Values{} v.Set("key", o.Key) v.Set("action", "shorten") v.Add("url", u) au, err := url.Parse(o.APIRoot + o.APIShortenEndpoint) if err != nil { return nil, err } au.RawQuery = v.Encode() req, err := http.NewRequest("GET", au.String(), nil) if err != nil { return nil, err } req = req.WithContext(ctx) resp, err := o.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, ErrRequestFailed } respstr, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } shortened = append(shortened, string(respstr)) } return }
package owogo import ( "bufio" "io" "net/http" "strings" "github.com/pkg/errors" ) const ( // DomainListURL is the link to the official public cdn list DomainListURL = "https://whats-th.is/public-cdn-domains.txt" // DefaultFilesKey is the domain list key for the default files endpoint DefaultFilesKey = "default-files" // DefaultLinksKey is the domain list key for the default links endpoint DefaultLinksKey = "default-links" ) // GetDomainListFile retrieves the official domain list as a raw string func GetDomainListFile() (string, error) { res, err := http.Get(DomainListURL) if err != nil { return "", errors.Wrap(err, "failed to download domain list") } defer res.Body.Close() var buffer []byte _, err = io.ReadFull(res.Body, buffer) if err != nil { return "", errors.Wrap(err, "failed to read domain list") } return string(buffer), nil } // DomainList holds all the available domains with some helper functions type DomainList struct { DefaultFiles string DefaultLinks string WildcardDomains []string Domains []string } // NewDomainList creates a new DomainList from the official list func NewDomainList() (*DomainList, error) { rawList, err := GetDomainListFile() if err != nil { return nil, err } list := new(DomainList) err = list.initialize(rawList) if err != nil { return nil, err } return list, nil } func (l *DomainList) initialize(raw string) error { rdr := bufio.NewReader(strings.NewReader(raw)) var line string var prefixed bool for { buffer, isPrefix, err := rdr.ReadLine() if err != nil { if err == io.EOF { return nil } return err } if prefixed && !isPrefix { prefixed = false } else if isPrefix { prefixed = true line += string(buffer) continue } line = strings.TrimSpace(line) // ignore comments if strings.HasPrefix(line, "#") { continue } else if strings.Contains(line, ":") { // action lines parts := strings.SplitN(line, ":", 2) switch parts[0] { case DefaultFilesKey: l.DefaultFiles = parts[1] case DefaultLinksKey: l.DefaultLinks = parts[1] default: return ErrUnknownKey } } else if strings.HasPrefix(line, "*") { // wildcard domains l.WildcardDomains = append(l.WildcardDomains, line[1:]) } else { // regular domains l.Domains = append(l.Domains, line) } // reset line line = "" } } // IsWildcardedDomain checks if the given domain is in the domainlist. // It strips the subdomain from the domain and checks if the base is listed. func (l *DomainList) IsWildcardedDomain(domain string) bool { if len(l.WildcardDomains) == 0 { return false } idx := strings.Index(domain, ".") if idx < 0 { return false } domain = strings.ToLower(domain[idx:]) for _, d := range l.WildcardDomains { if domain == strings.ToLower(d) { return true } } return false } // HasDomain checks if the given domain is available in the wildcarded or regular domains func (l *DomainList) HasDomain(domain string) bool { if l.IsWildcardedDomain(domain) { return true } else if len(l.Domains) == 0 { return false } else { domain = strings.ToLower(domain) for _, d := range l.Domains { if domain == strings.ToLower(d) { return true } } return false } }
package owogo import ( "io" "os" ) // NamedReader wrapper for a single file to upload type NamedReader struct { Reader io.Reader Filename string } // NewNamedReaderFromFilesystem creates a new reader from a filesystem path func NewNamedReaderFromFilesystem(path string) (*NamedReader, error) { file, err := os.Open(path) if err != nil { return nil, err } stat, err := file.Stat() if err != nil { return nil, err } if stat.IsDir() { return nil, ErrDirectory } if stat.Size() > FileUploadLimit { return nil, ErrFileToBig } return &NamedReader{ Filename: stat.Name(), Reader: file, }, nil }
// MIT License // Copyright (c) 2016 @zet4 / @Zeta#2229 / <my-name-is-zeta@and.my.foxgirlsare.sexy> // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package owogo type ( // Response contains json marshalled response from owo Response struct { Success bool `json:"success"` Errorcode int `json:"errorcode"` Description string `json:"description"` Files []File `json:"files"` } // File represents a single file from json response (if there were no errors) File struct { Hash string `json:"hash,omitempty"` Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` Size int `json:"size,omitempty"` Error bool `json:"error,omitempty"` Errorcode int `json:"errorcode,omitempty"` Description string `json:"description,omitempty"` } ) // WithCDN returns file url prefixed with the CDN func (f File) WithCDN(cdn string) (string, error) { if f.Error { return "", ErrRequestFailed } return cdn + f.URL, nil }