Maple Juice 2020 wrapping up

Well it’s been a weird season, it started as it always does on a nice late February day. But the Maples stopped flowing rather abruptly in the middle of March. It’s not unusual to get a break in the flow, but they never picked up again even on perfect days. I’m not sure what conclusions to draw, or if I care to draw any. But I’ve never seen it stop like that. Every single of the few seasons we’ve had so far, the trees wouldn’t just stop running until the sap turned yellow. Oh well, we still pulled 5 gallons and I still have about 2 more in the evaporator I’ll process very soon now that I know there won’t be any more sap. To add to the weirdness, all the syrup we made is super dark, no light early flow.

It’s a bit too bad, I was ready for a lot more. 7 Gallons is hard to complain about though.

One of the nicest things this year, I rigged a pump on the ATV to pump the sap straight into the tanks. I went from lifting hundreds of gallons of sap 3 times (bucket -> ATV -> tank -> evaporator), to only once (bucket -> ATV).

Ready to draw

drawing

And the yummy result (I have yet to commission labels for the adventures involved)

PHP Zoom API JWT Bit Banging

It’s always hard to nail the exact sequence when authorizing through a new API. Here’s what I came up with for PHP authorization with Zoom’s JWTs. This is essentially a quick start which gets you enough functions to do a first API call: to list zoom users. Line 40->68 is where the JWt meat happens.

<?php

// config parameters you need to define
define( 'ZOOM_TOKEN_FILE', "/var/.zoom_token" ) ; // some location on the filesystem used to cache your token while it's current (make sure permissions are restrictive)
define( 'ZOOM_API_KEY', "" ) ;
define( 'ZOOM_API_SECRET', "" ) ;


// main
print_r( zoom_list_users() ) ;
exit( 0 ) ;


// functions
function zoom_list_users() {
    $users = array() ;

    $page_number = 1 ;
    $keep_going = true ;
    while( $keep_going && $page_number<10000 ) {
        $result = zoom_make_api_call( "GET", "https://api.zoom.us/v2/users", array('page_size'=>300, 'page_number'=>$page_number, 'status'=>"active") ) ;
        $result = json_decode( $result, true ) ;
        if( array_key_exists('users', $result) &&
            count($result['users'])>0 ) {
            foreach( $result['users'] as $user ) {
                $users[] = $user ;
            }
            $page_number++ ;
            if( $page_number>$result['page_count'] ) {
                $keep_going = false ;
            }
        } else {
            $keep_going = false ;
        }
    }

    return $users ;
}

// PHP's default base64 encode isn't URL safe which messes up the JWT, we need these functions instead
function base64_url_encode( $data ) {
    return rtrim( strtr(base64_encode($data), '+/', '-_'), '=' ) ;
}
function base64_url_decode( $data ) {
    return base64_decode( str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT) ) ;
}

function get_token( $refresh=false ) {
    if( $refresh===false &&
        file_exists(ZOOM_TOKEN_FILE) ) {
      	return file_get_contents( ZOOM_TOKEN_FILE ) ;
    }

    $jwt_request_date    = @date( "U" ) ; // no warning, proper system timezone assumed
    $jwt_expiration_date = $jwt_request_date + 60*60 ; # +1 hour
    $jwt_header          = '{"alg":"HS256","typ":"JWT"}' ;
    $jwt_claim_set       = '{"iss":"' . ZOOM_API_KEY . '","exp":' . $jwt_expiration_date . '}' ;
    $jwt_signature       = sign_data( base64_url_encode($jwt_header) . '.' . base64_url_encode($jwt_claim_set), ZOOM_API_SECRET ) ;
    $jwt                 = base64_url_encode( $jwt_header ) . "." . base64_url_encode( $jwt_claim_set ) . "." . base64_url_encode( $jwt_signature ) ;

    file_put_contents( ZOOM_TOKEN_FILE, $jwt ) ;
    return $jwt ;
}


function sign_data( $data, $key ) {
    return hash_hmac( "SHA256" , $data, $key, true) ;
}


function zoom_make_api_call( $request, $url, $get_variables=null, $post_variables=null, $force_refresh_token=false ) {
    $ch = curl_init() ;
    $token = get_token( $force_refresh_token ) ;

    $getfields = "" ;
    if( $get_variables!==null && is_array($get_variables) ) {
        foreach( $get_variables as $get_variable_key=>$get_variable_value ) {
            $getfields .= "&" . urlencode( $get_variable_key ) . "=" . urlencode( $get_variable_value ) ;
        }
        if( strlen($getfields)>0 ) {
            $getfields = "?" . substr( $getfields, 1 ) ;
        }
    }

    curl_setopt( $ch, CURLOPT_URL, "{$url}{$getfields}" ) ;
    curl_setopt( $ch, CURLOPT_PORT , 443 ) ;
    curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $request ) ;
    curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ) ;
    if( $post_variables!==null && is_array($post_variables) ) {
        $postfields = "" ;
        foreach( $post_variables as $post_variable_key=>$post_variable_value ) {
            $postfields .= "&" . urlencode( $post_variable_key ) . "=" . urlencode( $post_variable_value ) ;
        }
        $postfields = substr( $postfields, 1 ) ;
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $postfields ) ;
    } else if( $post_variables!==null && is_string($post_variables) ) {
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_variables ) ;
    }
    curl_setopt( $ch, CURLOPT_HTTPHEADER, array( "authorization: Bearer {$token}",
                                                 "content-type: application/json") ) ;

    curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ) ;
    curl_setopt( $ch, CURLOPT_HEADER, true ) ;

    $response = curl_exec( $ch ) ;
    curl_close( $ch ) ;

    $response = parse_http_response( $response ) ;

    if( $response['code']=="200" ||
        $response['code']=="204" ||
        $response['code']=="404" ) {
        return $response['body'] ;
    } else if( $response['code']=="401" ) { // expired token
        if( $force_refresh_token===false ) {
            // safe to recurse
            return zoom_make_api_call( $request, $url, $get_variables, $post_variables, true ) ;
        } else {
            echo "ERROR: had an expired token and I tried to refresh it, yet somehow it's still expired\n" ;
            print_r( $response ) ;
            exit( 1 ) ;
        }
    } else {
        echo "ERROR: I have no idea what to do with this response from Zoom\n" ;
        print_r( $response ) ;
        exit( 1 ) ;
    }
}


function parse_http_response( $raw_data ) {
    $parsed_response = array( 'code'=>-1, 'headers'=>array(), 'body'=>"" ) ;

    $raw_data = explode( "\r\n", $raw_data ) ;

    $parsed_response['code'] = explode( " ", $raw_data[0] ) ;
    $parsed_response['code'] = $parsed_response['code'][1] ;
    $i = 1 ;
    if( $parsed_response['code']=="100" ) {
        $parsed_response['code'] = explode( " ", $raw_data[2] ) ;
        $parsed_response['code'] = $parsed_response['code'][1] ;
        $i = 3 ;
    }

    for( ; $i<count($raw_data) ; $i++ ) {
        $raw_datum = $raw_data[$i] ;

        $raw_datum = trim( $raw_datum ) ;
        if( $raw_datum!="" ) {
            if( substr_count($raw_datum, ':')>=1 ) {
                $raw_datum = explode( ':', $raw_datum, 2 ) ;
                $parsed_response['headers'][strtolower($raw_datum[0])] = trim( $raw_datum[1] ) ;
            }  else {
                echo "ERROR: we're in the headers section of parsing an HTTP section and no colon was found for line: {$raw_datum}\n" ;
                exit( 1 ) ;
            }
        } else {
            // we've moved to the body section
            if( ($i+1)<count($raw_data) ) {
                for( $j=($i+1) ; $j<count($raw_data) ; $j++ ) {
                    $parsed_response['body'] .= $raw_data[$j] . "\n" ;
                }
            }

            // we don't need to continue the $i loop
            break ;
        }
    }

    return $parsed_response ;
}

?>

Grand Opening

The Sugarhouse is officially open for business. I finished everything in the nick of time for the 2020 sugaring season. Which hit like a ton of bricks a little later than the usual mid-February.

Coming up is a list of all the cool features of the Sugarhouse.

I used the opportunity to touch up the bricking of the evaporator and add another layer where the fire burns the hotest.

 

The sap tanks are now above the evaporator to they can gravity feed into it. No more filling it one pot at a time :). The flue goes out through the cupola so as to create a draft of hot air going up around it, steering the vapor on the right path out the building.

Not only is the sap gravity fed, it’s self regulating thanks to a float valve.

I have a legit workbench for the first time in a decade.

No stain has been applied to the inside of the siding, I didn’t want vapor interacting with chemical so the inside is 100% untreated wood.

We have a very enclosed loft for kids to play into and stay away from the burning hot evaporator. I made little windows so they can watch without going over the railing. There is also a small basket they can play with to pass things up and down (a huge hit).

Inside the loft.

The pulley system which opens the cupola’s flaps. No ladders :).

The cupola in action.

First firing! A big moment for us.

The cupola’s capacity for evacuating vapor is much higher than our evaporator’s ability to make it. We’ll be able to upgrade it without worrying about vapor accumulation.

All in all it’s been a tremendous success and really super nice.