// 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
}